深入浅出:两种经典分治算法的 Python 实现
深入浅出:两种经典分治算法的 Python 实现
在计算机科学中,分治算法是一种非常重要的解题思想。它的核心思想是 “分而治之”,即将一个复杂的问题分解成若干个相似的子问题,递归解决子问题后再将结果合并。今天我们将通过两个经典案例 —— 循环赛日程表和棋盘覆盖问题,来深入理解分治算法的魅力。
一、循环赛日程表算法
问题描述
假设有 n=2^k 个运动员要进行网球循环赛,我们需要设计一个满足以下要求的比赛日程表:
-
每个选手必须与其他 n-1 个选手各赛一场
-
每个选手一天只能赛一次
-
循环赛一共进行 n-1 天
问题分析
我们可以将比赛日程表设计成一个 n×n 的表格,其中第 i 行第 j 列表示第 i 个选手在第 j 天的对手。
算法思路
循环赛日程表问题非常适合用分治算法来解决。其核心思路是:
-
基础情况:当只有 1 个选手时,日程表就是他自己
-
分治策略:将所有选手分为两半,递归地为每一半生成日程表
-
合并策略:
- 左上角子矩阵的内容复制到右下角
- 左下角子矩阵的内容复制到右上角
这种方法保证了比赛安排的公平性和完整性。
Python 实现代码
def generate_tournament_schedule(k):
"""
生成n=2^k个选手的循环赛日程表
参数:
k: 指数,决定选手数量n=2^k
返回:
日程表二维列表,schedule[i][j]表示第i+1个选手在第j+1天的对手
"""
n = 2 ** k # 选手数量
# 初始化日程表,n行n列,第0列表示选手编号
schedule = [[0] * n for _ in range(n)]
# 填充第一行(选手1的日程)
for j in range(n):
schedule[0][j] = j + 1
# 分治填充日程表
m = 1 # 当前子矩阵大小的一半
while m < n:
# 遍历每个子矩阵
for i in range(0, n, m * 2):
# 处理右上角子矩阵
for j in range(m, m * 2):
# 从左上角子矩阵复制并加上偏移量
for x in range(i, i + m):
schedule[x][j] = schedule[x][j - m] + m
# 处理左下角子矩阵
for j in range(0, m):
# 从右上角子矩阵复制
for x in range(i + m, i + m * 2):
schedule[x][j] = schedule[x - m][j + m]
# 处理右下角子矩阵
for j in range(m, m * 2):
# 从左上角子矩阵复制
for x in range(i + m, i + m * 2):
schedule[x][j] = schedule[x - m][j - m]
# print(f"第{m}次")
# for i in range(n):
# print(schedule[i])
m *= 2 # 子矩阵大小翻倍
return schedule
# 示例:生成8个选手(k=3)的赛程表
if __name__ == "__main__":
k = 3 # 8个选手
schedule = generate_tournament_schedule(k)
n = 2 ** k # 获取选手数量(整数)
for i in range(n):
print(schedule[i])
运行结果示例
[1, 2, 3, 4, 5, 6, 7, 8]
[2, 1, 4, 3, 6, 5, 8, 7]
[3, 4, 1, 2, 7, 8, 5, 6]
[4, 3, 2, 1, 8, 7, 6, 5]
[5, 6, 7, 8, 1, 2, 3, 4]
[6, 5, 8, 7, 2, 1, 4, 3]
[7, 8, 5, 6, 3, 4, 1, 2]
[8, 7, 6, 5, 4, 3, 2, 1]
二、棋盘覆盖算法
问题描述
在一个 2k×2k 的方格组成的棋盘中,有一个方格与其他方格不同,称之为奇异块。要求使用 L 型骨牌(每个骨牌覆盖 3 个方格)覆盖除奇异块外的其他所有方格,且骨牌之间不能互相覆盖。
算法思路
棋盘覆盖问题是分治算法的经典应用。其核心思想是:
-
基础情况:当棋盘大小为 1×1 时,不需要覆盖
-
分治策略:将 2k×2k 棋盘分为四个 2(k-1)×2(k-1) 的子棋盘
-
覆盖策略:
-
如果奇异块在某个子棋盘中,递归处理该子棋盘
-
对于没有奇异块的三个子棋盘,用一个 L 型骨牌覆盖它们的中心位置
-
将骨牌覆盖的三个方格视为新的 “奇异块”,继续递归处理
Python 实现代码
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import LinearSegmentedColormap
# 设置中文显示
plt.rcParams["font.family"] = ["SimHei"]
plt.rcParams["axes.unicode_minus"] = False # 解决负号显示问题
plt.rcParams["font.size"] = 14 # 全局默认字体大小(默认10,可改11、12等)
def chessboard_cover(board, tr, tc, dr, dc, size, tile):
if size == 1:
return
s = size // 2
t = tile[0]
tile[0] += 1
# 1. 处理左上子棋盘
if dr < tr + s and dc < tc + s:
chessboard_cover(board, tr, tc, dr, dc, s, tile)
else:
board[tr + s - 1][tc + s - 1] = t
chessboard_cover(board, tr, tc, tr + s - 1, tc + s - 1, s, tile)
# 4. 处理右下子棋盘
if dr >= tr + s and dc >= tc + s:
chessboard_cover(board, tr + s, tc + s, dr, dc, s, tile)
else:
board[tr + s][tc + s] = t
chessboard_cover(board, tr + s, tc + s, tr + s, tc + s, s, tile)
# 3. 处理左下子棋盘
if dr >= tr + s and dc < tc + s:
chessboard_cover(board, tr + s, tc, dr, dc, s, tile)
else:
board[tr + s][tc + s - 1] = t
chessboard_cover(board, tr + s, tc, tr + s, tc + s - 1, s, tile)
# 2. 处理右上子棋盘
if dr < tr + s and dc >= tc + s:
chessboard_cover(board, tr, tc + s, dr, dc, s, tile)
else:
board[tr + s - 1][tc + s] = t
chessboard_cover(board, tr, tc + s, tr + s - 1, tc + s, s, tile)
# 初始化参数
k = 3 # 8x8棋盘
size = 2 ** k
board = [[0 for _ in range(size)] for _ in range(size)]
dr, dc = 2, 3 # 奇异块位置(1,1)
board[dr][dc] = -1 # 奇异块标记为-1(区别于骨牌编号)
tile = [1]
chessboard_cover(board, 0, 0, dr, dc, size, tile)
board_np = np.array(board)
# 打印结果
for row in board:
print(row)
# 热力图
plt.imshow(board_np, cmap="hot",)
# 图例
plt.colorbar()
plt.show()
运行结果示例
[3, 3, 6, 6, 18, 18, 21, 21]
[3, 2, 2, 6, 18, 17, 17, 21]
[5, 2, 4, -1, 20, 20, 17, 19]
[5, 5, 4, 4, 1, 20, 19, 19]
[13, 13, 16, 1, 1, 8, 11, 11]
[13, 12, 16, 16, 8, 8, 7, 11]
[15, 12, 12, 14, 10, 7, 7, 9]
[15, 15, 14, 14, 10, 10, 9, 9]

三、算法分析与总结
循环赛日程表算法分析
时间复杂度:O (n²),其中 n=2^k。算法需要填充 n×n 的表格,每个元素都被处理一次。
空间复杂度:O (n²),主要用于存储日程表矩阵。
算法特点:
-
保证了每个选手与其他所有选手恰好比赛一次
-
每个选手每天只比赛一次
-
比赛安排具有对称性和规律性
棋盘覆盖算法分析
时间复杂度:O (4^k) = O (n²),其中 n=2^k。每次递归将问题分解为 4 个子问题,递归深度为 k。
空间复杂度:O (k),主要用于递归调用栈。
算法特点:
-
能够完美覆盖除奇异块外的所有方格
-
每个骨牌都是 L 型的
-
骨牌之间没有重叠
分治算法的核心思想
通过这两个案例,我们可以总结分治算法的核心思想:
-
分解(Divide):将复杂问题分解为若干个相似的子问题
-
解决(Conquer):递归解决子问题
-
合并(Combine):将子问题的解合并为原问题的解
分治算法特别适合那些具有以下特征的问题:
-
问题可以分解为相似的子问题
-
子问题的解可以合并为原问题的解
-
子问题之间相互独立
实际应用场景
循环赛日程表算法:
-
体育比赛安排
-
日程调度系统
-
资源分配问题
棋盘覆盖算法:
-
集成电路设计
-
图案填充问题
-
资源覆盖优化
这两种算法不仅展示了分治思想的强大威力,也为我们解决实际问题提供了宝贵的思路。通过学习这些经典算法,我们可以更好地理解递归思维和问题分解的艺术。
更多推荐
所有评论(0)