深入浅出:两种经典分治算法的 Python 实现

在计算机科学中,分治算法是一种非常重要的解题思想。它的核心思想是 “分而治之”,即将一个复杂的问题分解成若干个相似的子问题,递归解决子问题后再将结果合并。今天我们将通过两个经典案例 —— 循环赛日程表和棋盘覆盖问题,来深入理解分治算法的魅力。

一、循环赛日程表算法

问题描述

假设有 n=2^k 个运动员要进行网球循环赛,我们需要设计一个满足以下要求的比赛日程表:

  1. 每个选手必须与其他 n-1 个选手各赛一场

  2. 每个选手一天只能赛一次

  3. 循环赛一共进行 n-1 天

问题分析

我们可以将比赛日程表设计成一个 n×n 的表格,其中第 i 行第 j 列表示第 i 个选手在第 j 天的对手。

算法思路

循环赛日程表问题非常适合用分治算法来解决。其核心思路是:

  1. 基础情况:当只有 1 个选手时,日程表就是他自己

  2. 分治策略:将所有选手分为两半,递归地为每一半生成日程表

  3. 合并策略

  • 左上角子矩阵的内容复制到右下角
  • 左下角子矩阵的内容复制到右上角
    这种方法保证了比赛安排的公平性和完整性。

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×1 时,不需要覆盖

  2. 分治策略:将 2k×2k 棋盘分为四个 2(k-1)×2(k-1) 的子棋盘

  3. 覆盖策略

  • 如果奇异块在某个子棋盘中,递归处理该子棋盘

  • 对于没有奇异块的三个子棋盘,用一个 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 型的

  • 骨牌之间没有重叠

分治算法的核心思想

通过这两个案例,我们可以总结分治算法的核心思想:

  1. 分解(Divide):将复杂问题分解为若干个相似的子问题

  2. 解决(Conquer):递归解决子问题

  3. 合并(Combine):将子问题的解合并为原问题的解

分治算法特别适合那些具有以下特征的问题:

  • 问题可以分解为相似的子问题

  • 子问题的解可以合并为原问题的解

  • 子问题之间相互独立

实际应用场景

循环赛日程表算法

  • 体育比赛安排

  • 日程调度系统

  • 资源分配问题

棋盘覆盖算法

  • 集成电路设计

  • 图案填充问题

  • 资源覆盖优化

这两种算法不仅展示了分治思想的强大威力,也为我们解决实际问题提供了宝贵的思路。通过学习这些经典算法,我们可以更好地理解递归思维和问题分解的艺术。


Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐