【算法】【《计算机算法设计与分析(第5版)》笔记】第二章:递归与分治策略

2023-12-15 11:34:04

前导

【算法】【《计算机算法设计与分析(第5版)》笔记】第一章:算法概述

2.1|递归的概念

F i b o n a c c i Fibonacci Fibonacci数列非递归定义

F ( n ) = 1 5 [ ( 1 + 5 2 ) n + 1 ? ( 1 ? 5 2 ) n + 1 ] F(n) = \cfrac{1}{\sqrt{5}} \left[\left(\cfrac{1 + \sqrt{5}}{2}\right)^{n + 1} - \left(\cfrac{1 - \sqrt{5}}{2}\right)^{n + 1}\right] F(n)=5 ?1? ?(21+5 ??)n+1?(21?5 ??)n+1 ?

A c k e r m a n Ackerman Ackerman函数
  • 并非一切递归函数都能用非递归方式定义
  • A c k e r m a n Ackerman Ackerman函数是一个双递归函数,当一个函数及它的一个变量由函数自身定义时,称这个函数是双递归函数
递归方程

A ( n , m ) = { 2 n = 1 , m = 0 1 n = 0 , m ≥ 0 n + 2 n ≥ 2 , m = 0 A ( A ( n ? 1 , m ) , m ? 1 ) n , m ≥ 1 A(n , m) = \begin{cases} 2 & n = 1 , m = 0 \\ 1 & n = 0 , m \geq 0 \\ n + 2 & n \geq 2 , m = 0 \\ A(A(n - 1 , m) , m - 1) & n , m \geq 1 \end{cases} A(n,m)=? ? ??21n+2A(A(n?1,m),m?1)?n=1,m=0n=0,m0n2,m=0n,m1?

  • A ( n , m ) A(n , m) A(n,m)的自变量 m m m的每个值都定义了一个单变量函数
    • m = 0 m = 0 m=0时,定义了函数“加 2 2 2
    • m = 1 m = 1 m=1时,由于 A ( 1 , 1 ) = A ( A ( 0 , 1 ) , 0 ) = A ( 1 , 0 ) = 2 A(1 , 1) = A(A(0 , 1) , 0) = A(1 , 0) = 2 A(1,1)=A(A(0,1),0)=A(1,0)=2,以及 A ( n , 1 ) = A ( A ( n ? 1 , 1 ) , 0 ) = A ( n ? 1 , 1 ) + 2 ( n > 1 ) A(n , 1) = A(A(n - 1 , 1) , 0) = A(n - 1 , 1) + 2 (n > 1) A(n,1)=A(A(n?1,1),0)=A(n?1,1)+2(n>1),因此 A ( n , 1 ) = 2 n ( n ≥ 1 ) A(n , 1) = 2n (n \geq 1) A(n,1)=2n(n1),即 A ( n , 1 ) A(n , 1) A(n,1)是函数“乘 2 2 2
    • m = 2 m = 2 m=2时, A ( n , 2 ) = A ( A ( n ? 1 , 2 ) , 1 ) = 2 A ( n ? 1 , 2 ) A(n , 2) = A(A(n - 1 , 2) , 1) = 2 A(n - 1 , 2) A(n,2)=A(A(n?1,2),1)=2A(n?1,2) A ( 1 , 2 ) = A ( A ( 0 , 2 ) , 1 ) = A ( 1 , 1 ) = 2 A(1 , 2) = A(A(0 , 2) , 1) = A(1 , 1) = 2 A(1,2)=A(A(0,2),1)=A(1,1)=2,故 A ( n , 2 ) = 2 n A(n , 2) = 2^{n} A(n,2)=2n
    • 类似地可以推出 A ( n , 3 ) = 2 2 ╱ 2 A(n , 3) = 2^{2^{\diagup^{2}}} A(n,3)=222,其中 2 2 2的层数为 n n n
    • A ( n , 4 ) A(n , 4) A(n,4)的增长速度非常快,以至于没有适当的数学式子来表示这一函数
  • 单变量的 A c k e r m a n Ackerman Ackerman函数 A ( n ) A(n) A(n)定义为 A ( n ) = A ( n , n ) A(n) = A(n , n) A(n)=A(n,n),其拟逆函数 α ( n ) = min ? { ? k ∣ A ( k ) ≥ n ? } \alpha(n) = \min\set{k \mid A(k) \geq n} α(n)=min{kA(k)n}
    • A ( 4 ) = 2 2 ╱ 2 A(4) = 2^{2^{\diagup^{2}}} A(4)=222(其中 2 2 2的层数为 65536 65536 65536)的值非常大,如果要写出这个数将需要 log ? ( A ( 4 ) ) \log(A(4)) log(A(4))位,即 2 2 ╱ 2 2^{2^{\diagup^{2}}} 222 65535 65535 65535 2 2 2的方幂)位,所以对于通常见到的正整数 n n n,有 α ( n ) ≤ 4 \alpha(n) \leq 4 α(n)4
    • 在理论上 α ( n ) \alpha(n) α(n)没有上界
Python实现
def ackerman(n, m):
    if n == 1 and m == 0:
        return 2
    elif n == 0 and m >= 0:
        return 1
    elif n >= 2 and m == 0:
        return n + 2
    elif n >= 1 and m >= 1:
        return ackerman(ackerman(n - 1, m), m - 1)


res = ackerman(3, 3)

print(res)
排列问题
问题描述
  • R = { ? r 1 , r 2 , ? ? , r n ? } R = \set{r_{1} , r_{2} , \cdots , r_{n}} R={r1?,r2?,?,rn?}是要进行排列的 n n n个元素, R i = R ? { ? r i ? } R_{i} = R - \set{r_{i}} Ri?=R?{ri?}
分治算法
  • 集合 X X X中的元素的全排列记为 P e r m ( X ) Perm(X) Perm(X) ( r i ) P e r m ( X ) (r_{i}) Perm(X) (ri?)Perm(X)表示在全排列 P e r m ( X ) Perm(X) Perm(X)的每个排列前加上前缀 r i r_{i} ri?得到的排列

  • R R R的全排列可递归定义为

    • n = 1 n = 1 n=1时, P e r m ( R ) = ( r ) Perm(R) = (r) Perm(R)=(r),其中 r r r是集合 R R R中唯一的元素

    • n > 1 n > 1 n>1时, P e r m ( R ) Perm(R) Perm(R) ( r 1 ) P e r m ( R 1 ) (r_{1}) Perm(R_{1}) (r1?)Perm(R1?) ( r 2 ) P e r m ( R 2 ) (r_{2}) Perm(R_{2}) (r2?)Perm(R2?) ? \cdots ? ( r n ) P e r m ( R n ) (r_{n}) Perm(R_{n}) (rn?)Perm(Rn?)构成

  • 递归地产生所有前缀是num[0:k - 1],且后缀是num[k:m]的全排列的所有排列

Python实现
def permute(nums):
    if len(nums) <= 1:
        return [nums]

    result = []

    for i in range(len(nums)):
        m = nums[i]
        remaining_nums = nums[:i] + nums[i + 1:]

        sub_permutations = permute(remaining_nums)

        for p in sub_permutations:
            result.append([m] + p)

    return result


nums = [1, 2, 3]

permutations = permute(nums)

for p in permutations:
    print(p)
整数划分问题
问题描述
  • 将正整数 n n n表示成一系列正整数之和, n = n 1 + n 2 + ? + n k ( n 1 ≥ n 2 ≥ ? ≥ n k ≥ 1 , k ≥ 1 ) n = n_{1} + n_{2} + \cdots + n_{k} (n_{1} \geq n_{2} \geq \cdots \geq n_{k} \geq 1 , k \geq 1) n=n1?+n2?+?+nk?(n1?n2??nk?1,k1)
  • 正整数 n n n的这种表示称为正整数 n n n的划分,正整数 n n n的不同的划分个数称为正整数 n n n的划分数,记为 p ( n ) p(n) p(n)
分治算法
  • 在正整数 n n n的所有划分中,将最大加数 n 1 n_{1} n1?不大于 m m m的划分个数记作 q ( n , m ) q(n , m) q(n,m),可以建立 q ( n , m ) q(n , m) q(n,m)的递归关系

q ( n , m ) = { 1 , n = 1 , m = 1 q ( n , n ) , n < m 1 + q ( n , n ? 1 ) , n = m q ( n , m ? 1 ) + q ( n ? m , m ) , n > m > 1 q(n , m) = \begin{cases} 1 , & n = 1 , m = 1 \\ q(n , n) , & n < m \\ 1 + q(n , n - 1) , & n = m \\ q(n , m - 1) + q(n - m , m) , & n > m > 1 \end{cases} q(n,m)=? ? ??1,q(n,n),1+q(n,n?1),q(n,m?1)+q(n?m,m),?n=1,m=1n<mn=mn>m>1?

Python实现
def integer_partition(n, m):
    if n < 1 or m < 1:
        return 0

    if n == 1 or m == 1:
        return 1

    if n < m:
        return integer_partition(n, n)

    if n == m:
        return integer_partition(n, m - 1) + 1

    return integer_partition(n, m - 1) + integer_partition(n - m, m)


n = 6

res = integer_partition(n, n)

print(f'The number of partitions for {n} is: {res}')
Hanoi塔问题
问题描述
  • a a a b b b c c c是三个塔座,开始时,在塔座 a a a上有一叠共 n n n个圆盘,这些圆盘自下而上,由大到小地叠放在一起,各圆盘从小到大编号为 1 1 1 2 2 2 ? \cdots ? n n n,要求将塔座 a a a上的这一叠圆盘移到塔座 b b b上,并仍按照同样顺序叠置
  • 在移动圆盘时应遵守以下移动规则
    • 每次只能移动一个圆盘
    • 任何时刻都不允许将较大的圆盘压在较小的圆盘之上
Python实现
def hanoi(n, source, target, auxiliary):
    if n > 0:
        # 将 n - 1 个盘子从源柱移动到辅助柱
        hanoi(n - 1, source, auxiliary, target)
        # 将第 n 个盘子从源柱移动到目标柱
        print(f'将盘子 {n}{source} 移动到 {target}')
        # 将 n - 1 个盘子从辅助柱移动到目标柱
        hanoi(n - 1, auxiliary, target, source)


n = 3

hanoi(n, 'A', 'B', 'C')

2.2|分治法的基本思想

分治法时间复杂性
  • k k k为子问题的个数, n / m n / m n/m为子问题的规模

T ( n ) = { O ( 1 ) n = 1 k T ( n / m ) + f ( n ) n > 1 T(n) = \begin{cases} O(1) & n = 1 \\ k T(n / m) + f(n) & n > 1 \end{cases} T(n)={O(1)kT(n/m)+f(n)?n=1n>1?

T ( n ) = n log ? m k + ∑ j = 0 log ? m n ? 1 k j f ( n / m j ) T(n) = n^{\log_{m}{k}} + \displaystyle\sum\limits_{j = 0}^{\log_{m}{n - 1}}{k^{j} f(n / m^{j})} T(n)=nlogm?k+j=0logm?n?1?kjf(n/mj)


2.3|二分搜索技术

问题描述
  • 给定已排好序的 n n n个元素a[0:n - 1],在这 n n n个元素中找出一特定元素 x x x
Python实现
def binary_search(arr, target):
    low = 0
    high = len(arr) - 1

    while low <= high:
        mid = (low + high) // 2

        if arr[mid] == target:
            return mid
        elif arr[mid] < target:
            low = mid + 1
        else:
            high = mid - 1

    return -1


arr = [1, 3, 5, 7, 9]
target = 5

res = binary_search(arr, target)

if res != -1:
    print(f'目标元素 {target} 在数组中的索引为 {res}')
else:
    print('目标元素不在数组中')
时间复杂性
  • 在最坏情况下,while循环被执行了 O ( log ? n ) O(\log{n}) O(logn)次,循环体内运算需要 O ( 1 ) O(1) O(1)时间,因此整个算法在最坏情况下的时间复杂性为 O ( log ? n ) O(\log{n}) O(logn)

2.4|大整数的乘法

问题描述
  • X X X Y Y Y都是 n n n位二进制整数,计算它们的乘积 X Y XY XY
基础算法
  • n n n位二进制整数 X X X Y Y Y都分为 2 2 2段,每段的长为 n / 2 n / 2 n/2位(假设 n n n 2 2 2的幂)
    • X = A × 2 n / 2 + B X = A \times 2^{n / 2} + B X=A×2n/2+B
    • Y = C × 2 n / 2 + D Y = C \times 2^{n / 2} + D Y=C×2n/2+D
    • X Y = ( A × 2 n / 2 + B ) ( C × 2 n / 2 + D ) = A C × 2 n + ( A D + B C ) × 2 n / 2 + B D XY = (A \times 2^{n / 2} + B) (C \times 2^{n / 2} + D) = AC \times 2^{n} + (AD + BC) \times 2^{n / 2} + BD XY=(A×2n/2+B)(C×2n/2+D)=AC×2n+(AD+BC)×2n/2+BD
时间复杂性
  • 如果按此式计算 X Y XY XY,必须进行 4 4 4 n / 2 n / 2 n/2位整数的乘法, 3 3 3次不超过 2 n 2n 2n位的整数加法,以及 2 2 2次移位,所有这些加法和移位共用 O ( n ) O(n) O(n)步运算

T ( n ) = { O ( 1 ) n = 1 4 T ( n / 2 ) + O ( n ) n > 1 T(n) = \begin{cases} O(1) & n = 1 \\ 4 T(n / 2) + O(n) & n > 1 \end{cases} T(n)={O(1)4T(n/2)+O(n)?n=1n>1?

T ( n ) = O ( n 2 ) T(n) = O(n^{2}) T(n)=O(n2)

优化算法
  • 要想改进算法的计算复杂性,必须减少乘法次数,把 X Y XY XY写成另一种形式
    • X Y = A C × 2 n + ( ( A ? B ) ( D ? C ) + A C + B D ) × 2 n / 2 + B D XY = AC \times 2^{n} + ((A - B)(D - C) + AC + BD) \times 2^{n / 2} + BD XY=AC×2n+((A?B)(D?C)+AC+BD)×2n/2+BD
时间复杂性
  • 需做 3 3 3 n / 2 n / 2 n/2位整数的乘法, 6 6 6次加减法和 2 2 2次移位

T ( n ) = { O ( 1 ) n = 1 3 T ( n / 2 ) + O ( n ) n > 1 T(n) = \begin{cases} O(1) & n = 1 \\ 3 T(n / 2) + O(n) & n > 1 \end{cases} T(n)={O(1)3T(n/2)+O(n)?n=1n>1?

T ( n ) = O ( n log ? 3 ) = O ( n 1.59 ) T(n) = O(n^{\log{3}}) = O(n^{1.59}) T(n)=O(nlog3)=O(n1.59)

Python实现
def karatsuba_multiply(x, y):
    # 如果乘数之一为 0, 则直接返回 0
    if x == 0 or y == 0:
        return 0

    # 将乘数转换为字符串, 并获取它们的位数
    x_str = str(x)
    y_str = str(y)

    n = max(len(x_str), len(y_str))

    # 达到基本情况时, 使用传统的乘法
    if n == 1:
        return x * y

    # 将乘数补齐到相同的位数
    x_str = x_str.zfill(n)
    y_str = y_str.zfill(n)

    # 将乘数划分为两部分
    m = n // 2

    high1, low1 = int(x_str[:m]), int(x_str[m:])
    high2, low2 = int(y_str[:m]), int(y_str[m:])

    # 递归地计算三个乘法
    z0 = karatsuba_multiply(low1, low2)
    z1 = karatsuba_multiply((low1 + high1), (low2 + high2))
    z2 = karatsuba_multiply(high1, high2)

    # 计算结果
    res = (z2 * 10 ** (2 * m)) + ((z1 - z2 - z0) * 10 ** m) + z0

    return res


x = 123456789012345678901234567890
y = 987654321098765432109876543210

res = karatsuba_multiply(x, y)

print(f'{x} * {y} = {res}')

2.5|Strassen矩阵乘法

问题描述
  • A A A B B B是两个 n × n n \times n n×n矩阵, A A A B B B的乘积矩阵 C C C中元素 c i j = ∑ k = 1 n a i k b k j c_{ij} = \displaystyle\sum\limits_{k = 1}^{n}{a_{ik} b_{kj}} cij?=k=1n?aik?bkj?
  • 每计算 C C C的一个元素 c i j c_{ij} cij?,需要做 n n n次乘法和 n ? 1 n - 1 n?1次加法,求出矩阵 C C C n 2 n^{2} n2个元素所需的时间为 O ( n 3 ) O(n^{3}) O(n3)
基础算法
  • 假设 n n n 2 2 2的幂,将矩阵 A A A B B B C C C中每个矩阵都分块成 4 4 4个大小相等的子矩阵,每个子矩阵都是 n / 2 × n / 2 n / 2 \times n / 2 n/2×n/2的方阵

∣ C 11 C 12 C 21 C 22 ∣ = ∣ A 11 A 12 A 21 A 22 ∣ ∣ B 11 B 12 B 21 B 22 ∣ \begin{vmatrix} C_{11} & C_{12} \\ C_{21} & C_{22} \end{vmatrix} = \begin{vmatrix} A_{11} & A_{12} \\ A_{21} & A_{22} \end{vmatrix} \begin{vmatrix} B_{11} & B_{12} \\ B_{21} & B_{22} \end{vmatrix} ?C11?C21??C12?C22?? ?= ?A11?A21??A12?A22?? ? ?B11?B21??B12?B22?? ?

C 11 = A 11 B 11 + A 12 B 21 C 12 = A 11 B 12 + A 12 B 22 C 21 = A 21 B 11 + A 22 B 21 C 22 = A 21 B 12 + A 22 B 22 C_{11} = A_{11} B_{11} + A_{12} B_{21} \\ C_{12} = A_{11} B_{12} + A_{12} B_{22} \\ C_{21} = A_{21} B_{11} + A_{22} B_{21} \\ C_{22} = A_{21} B_{12} + A_{22} B_{22} C11?=A11?B11?+A12?B21?C12?=A11?B12?+A12?B22?C21?=A21?B11?+A22?B21?C22?=A21?B12?+A22?B22?

时间复杂性
  • 计算 2 2 2 n n n阶方阵的乘积转化为计算 8 8 8 n / 2 n / 2 n/2阶方阵的乘积和 4 4 4 n / 2 n / 2 n/2阶方阵的加法, 2 2 2 n / 2 × n / 2 n / 2 \times n / 2 n/2×n/2矩阵的加法显然可以在 O ( n 2 ) O(n^{2}) O(n2)时间内完成

T ( n ) = { O ( 1 ) n = 2 8 T ( n / 2 ) + O ( n 2 ) n > 2 T(n) = \begin{cases} O(1) & n = 2 \\ 8 T(n / 2) + O(n^{2}) & n > 2 \end{cases} T(n)={O(1)8T(n/2)+O(n2)?n=2n>2?

T ( n ) = O ( n 3 ) T(n) = O(n^{3}) T(n)=O(n3)

Strassen算法
  • Strassen算法只用了 7 7 7次乘法运算,但增加了加减法的运算次数

M 1 = A 11 ( B 12 ? B 22 ) M 2 = ( A 11 + A 12 ) B 22 M 3 = ( A 21 + A 22 ) B 11 M 4 = A 22 ( B 21 ? B 11 ) M 5 = ( A 11 + A 22 ) ( B 11 + B 22 ) M 6 = ( A 12 ? A 22 ) ( B 21 + B 22 ) M 7 = ( A 11 ? A 21 ) ( B 11 + B 12 ) M_{1} = A_{11} (B_{12} - B_{22}) \\ M_{2} = (A_{11} + A_{12}) B_{22} \\ M_{3} = (A_{21} + A_{22}) B_{11} \\ M_{4} = A_{22} (B_{21} - B_{11}) \\ M_{5} = (A_{11} + A_{22}) (B_{11} + B_{22}) \\ M_{6} = (A_{12} - A_{22}) (B_{21} + B_{22}) \\ M_{7} = (A_{11} - A_{21}) (B_{11} + B_{12}) M1?=A11?(B12??B22?)M2?=(A11?+A12?)B22?M3?=(A21?+A22?)B11?M4?=A22?(B21??B11?)M5?=(A11?+A22?)(B11?+B22?)M6?=(A12??A22?)(B21?+B22?)M7?=(A11??A21?)(B11?+B12?)

C 11 = M 5 + M 4 ? M 2 + M 6 C 12 = M 1 + M 2 C 21 = M 3 + M 4 C 22 = M 5 + M 1 ? M 3 ? M 7 C_{11} = M_{5} + M_{4} - M_{2} + M_{6} \\ C_{12} = M_{1} + M_{2} \\ C_{21} = M_{3} + M_{4} \\ C_{22} = M_{5} + M_{1} - M_{3} - M_{7} C11?=M5?+M4??M2?+M6?C12?=M1?+M2?C21?=M3?+M4?C22?=M5?+M1??M3??M7?

时间复杂性
  • Strassen算法用了 7 7 7次对于 n / 2 n / 2 n/2阶矩阵乘积的递归调用和 18 18 18 n / 2 n / 2 n/2阶矩阵的加减运算

T ( n ) = { O ( 1 ) n = 2 7 T ( n / 2 ) + O ( n 2 ) n > 2 T(n) = \begin{cases} O(1) & n = 2 \\ 7 T(n / 2) + O(n^{2}) & n > 2 \end{cases} T(n)={O(1)7T(n/2)+O(n2)?n=2n>2?

T ( n ) = O ( n log ? 7 ) ≈ O ( n 2.81 ) T(n) = O(n^{\log{7}}) \approx O(n^{2.81}) T(n)=O(nlog7)O(n2.81)

问题时间复杂性
  • H o p c r o f t Hopcroft Hopcroft K e r r Kerr Kerr已经证明计算 2 2 2 2 × 2 2 \times 2 2×2矩阵的乘积, 7 7 7次乘法是必要的
  • 目前最好的计算时间上界是 O ( n 2.376 ) O(n^{2.376}) O(n2.376),所知的矩阵乘法的最好下界仍是它的平凡下界 Ω ( n 2 ) \Omega(n^{2}) Ω(n2)
Python实现
import numpy as np


def strassen_matrix_multiply(a, b):
    n = a.shape[0]

    # 如果输入矩阵的维度小于等于阈值, 使用传统的矩阵乘法
    if n <= 128:
        return np.dot(a, b)

    # 将输入矩阵划分为四个子矩阵
    mid = n // 2

    a11 = a[:mid, :mid]
    a12 = a[:mid, mid:]
    a21 = a[mid:, :mid]
    a22 = a[mid:, mid:]

    b11 = b[:mid, :mid]
    b12 = b[:mid, mid:]
    b21 = b[mid:, :mid]
    b22 = b[mid:, mid:]

    # 递归地计算七个矩阵乘法
    m1 = strassen_matrix_multiply(a11, b12 - b22)
    m2 = strassen_matrix_multiply(a11 + a12, b22)
    m3 = strassen_matrix_multiply(a21 + a22, b11)
    m4 = strassen_matrix_multiply(a22, b21 - b11)
    m5 = strassen_matrix_multiply(a11 + a22, b11 + b22)
    m6 = strassen_matrix_multiply(a12 - a22, b21 + b22)
    m7 = strassen_matrix_multiply(a11 - a21, b11 + b12)

    # 计算结果矩阵的四个子矩阵
    c11 = m5 + m4 - m2 + m6
    c12 = m1 + m2
    c21 = m3 + m4
    c22 = m5 + m1 - m3 - m7

    # 组合四个子矩阵形成结果矩阵
    c = np.zeros((n, n))

    c[:mid, :mid] = c11
    c[:mid, mid:] = c12
    c[mid:, :mid] = c21
    c[mid:, mid:] = c22

    return c


a = np.random.randint(0, 10, (4, 4))
b = np.random.randint(0, 10, (4, 4))

res = strassen_matrix_multiply(a, b)

print('矩阵a:')
print(a)

print('\n矩阵b:')
print(b)

print('\n乘积矩阵:')
print(res)

2.6|棋盘覆盖

问题描述
  • 在一个 2 k × 2 k 2^{k} \times 2^{k} 2k×2k个方格组成的棋盘中,若恰有一个方格与其他方格不同,则称该方格为一特殊方格,且称该棋盘为一特殊棋盘
  • 4 4 4种不同形态的 L L L型骨牌覆盖一个给定的特殊棋盘上除特殊方格以外的所有方格,且任何 2 2 2 L L L型骨牌不得重复覆盖
分治算法
  • k > 0 k > 0 k>0时,将 2 k × 2 k 2^{k} \times 2^{k} 2k×2k棋盘分割为 4 4 4 2 k ? 1 × 2 k ? 1 2^{k - 1} \times 2^{k - 1} 2k?1×2k?1子棋盘,特殊方格必位于 4 4 4个较小子棋盘之一中,其余 3 3 3个子棋盘中无特殊方格
  • 为了将这 3 3 3个无特殊方格的子棋盘转化为特殊棋盘,可以用一个 L L L型骨牌覆盖这 3 3 3个较小棋盘的会合处
时间复杂性

T ( k ) = { O ( 1 ) k = 0 4 T ( k ? 1 ) + O ( 1 ) k > 0 T(k) = \begin{cases} O(1) & k = 0 \\ 4 T(k - 1) + O(1) & k > 0 \end{cases} T(k)={O(1)4T(k?1)+O(1)?k=0k>0?

T ( k ) = O ( 4 k ) T(k) = O(4^{k}) T(k)=O(4k)

  • 由于覆盖一个 2 k × 2 k 2^{k} \times 2^{k} 2k×2k棋盘所需的 L L L型骨牌个数为 ( 4 k ? 1 ) / 3 (4^{k} - 1) / 3 (4k?1)/3,故算法是一个在渐进意义下最优的算法
Python实现
def chessboard_cover(board, tr, tc, dr, dc, size):
    """
    :param board: 棋盘
    :param tr: 棋盘左上角行号
    :param tc: 棋盘左上角列号
    :param dr: 特殊方格行号
    :param dc: 特殊方格列号
    :param size: 棋盘大小
    """
    global tile_count

    # 基本情况: 棋盘大小为 1, 直接放置骨牌
    if size == 1:
        return

    t = tile_count
    tile_count += 1

    # 将棋盘分成 4 个子棋盘
    s = size // 2

    # 左上子棋盘
    if dr < tr + s and dc < tc + s:
        chessboard_cover(board, tr, tc, dr, dc, s)
    else:
        board[tr + s - 1][tc + s - 1] = t
        chessboard_cover(board, tr, tc, tr + s - 1, tc + s - 1, s)

    # 右上子棋盘
    if dr < tr + s and dc >= tc + s:
        chessboard_cover(board, tr, tc + s, dr, dc, s)
    else:
        board[tr + s - 1][tc + s] = t
        chessboard_cover(board, tr, tc + s, tr + s - 1, tc + s, s)

    # 左下子棋盘
    if dr >= tr + s and dc < tc + s:
        chessboard_cover(board, tr + s, tc, dr, dc, s)
    else:
        board[tr + s][tc + s - 1] = t
        chessboard_cover(board, tr + s, tc, tr + s, tc + s - 1, s)

    # 右下子棋盘
    if dr >= tr + s and dc >= tc + s:
        chessboard_cover(board, tr + s, tc + s, dr, dc, s)
    else:
        board[tr + s][tc + s] = t
        chessboard_cover(board, tr + s, tc + s, tr + s, tc + s, s)


size = 8
board = [[0] * size for _ in range(size)]

special_row = 3
special_col = 4

board[special_row][special_col] = -1

tile_count = 0

chessboard_cover(board, 0, 0, special_row, special_col, size)

for row in board:
    print(row)

2.7|合并排序

递归算法
Python实现
def merge_sort(arr):
    # 基本情况: 当数组长度为 1 或 0 时, 直接返回
    if len(arr) <= 1:
        return arr

    # 将数组分成两半
    mid = len(arr) // 2
    left = arr[:mid]
    right = arr[mid:]

    # 递归地对左右两半进行排序
    left_sorted = merge_sort(left)
    right_sorted = merge_sort(right)

    # 合并已排序的左右两半
    merged = merge(left_sorted, right_sorted)

    return merged


def merge(left_sorted, right_sorted):
    merged = []
    i = j = 0

    # 比较左右两个数组的元素, 按顺序合并到结果数组中
    while i < len(left_sorted) and j < len(right_sorted):
        if left_sorted[i] <= right_sorted[j]:
            merged.append(left_sorted[i])
            i += 1
        else:
            merged.append(right_sorted[j])
            j += 1

    # 将剩余的元素添加到结果数组中
    while i < len(left_sorted):
        merged.append(left_sorted[i])
        i += 1

    while j < len(right_sorted):
        merged.append(right_sorted[j])
        j += 1

    return merged


arr = [5, 3, 8, 4, 2, 1, 6, 7]

sorted_arr = merge_sort(arr)

print(f'sorted_arr:', sorted_arr)
时间复杂性

T ( n ) = { O ( 1 ) n ≤ 1 2 T ( n / 2 ) + O ( n ) n > 1 T(n) = \begin{cases} O(1) & n \leq 1 \\ 2 T(n / 2) + O(n) & n > 1 \end{cases} T(n)={O(1)2T(n/2)+O(n)?n1n>1?

T ( n ) = O ( n log ? n ) T(n) = O(n \log{n}) T(n)=O(nlogn)

  • 由于排序问题的计算时间下界为 Ω ( n log ? n ) \Omega(n \log{n}) Ω(nlogn),故合并排序算法是一个渐进最优算法
非递归算法
Python实现
def merge_sort(arr):
    # 将列表中的每个元素转换为单个元素的子列表
    sublists = [[val] for val in arr]

    # 依次合并相邻的子列表, 直到只剩下一个排序好的列表
    while len(sublists) > 1:
        merged_sublists = []

        # 两两合并相邻的子列表
        for i in range(0, len(sublists), 2):
            sublist_1 = sublists[i]
            sublist_2 = sublists[i + 1] if i + 1 < len(sublists) else []

            merged = merge(sublist_1, sublist_2)
            merged_sublists.append(merged)

        sublists = merged_sublists

    # 返回最终的排序结果
    return sublists[0] if sublists else []


def merge(left_sorted, right_sorted):
    merged = []
    i = j = 0

    # 比较左右两个列表的元素, 按顺序合并到结果列表中
    while i < len(left_sorted) and j < len(right_sorted):
        if left_sorted[i] <= right_sorted[j]:
            merged.append(left_sorted[i])
            i += 1
        else:
            merged.append(right_sorted[j])
            j += 1

    # 将剩余的元素添加到结果列表中
    while i < len(left_sorted):
        merged.append(left_sorted[i])
        i += 1

    while j < len(right_sorted):
        merged.append(right_sorted[j])
        j += 1

    return merged


arr = [5, 3, 8, 4, 2, 1, 6, 7]

sorted_arr = merge_sort(arr)

print(f'sorted_arr:', sorted_arr)
自然合并排序
  • 1 1 1次对数组的线性扫描找出所有排好序的子数组段,然后将相邻的排好序的子数组段两两合并,构成更大的排好序的子数组段

2.8|快速排序

Python实现
def quick_sort(arr):
    if len(arr) <= 1:
        return arr

    pivot = arr[0]

    less = [x for x in arr[1:] if x <= pivot]
    greater = [x for x in arr[1:] if x > pivot]

    return quick_sort(less) + [pivot] + quick_sort(greater)


arr = [3, 1, 5, 2, 4]

sorted_arr = quick_sort(arr)

print(f'sorted_arr', sorted_arr)
时间复杂性
  • 快速排序的运行时间与划分是否对称有关,其最坏情况发生在划分过程产生的两个区域分别包含 n ? 1 n - 1 n?1个元素和 1 1 1个元素的时候,在最好情况下,每次划分所取的基准都恰好为中值,即每次划分都产生两个大小为 n / 2 n / 2 n/2的区域
最坏时间复杂性

T ( n ) = { O ( 1 ) n ≤ 1 T ( n ? 1 ) + O ( n ) n > 1 T(n) = \begin{cases} O(1) & n \leq 1 \\ T(n - 1) + O(n) & n > 1 \end{cases} T(n)={O(1)T(n?1)+O(n)?n1n>1?

T ( n ) = n 2 T(n) = n^{2} T(n)=n2

最好时间复杂性

T ( n ) = { O ( 1 ) n ≤ 1 2 T ( n / 2 ) + O ( n ) n > 1 T(n) = \begin{cases} O(1) & n \leq 1 \\ 2 T(n / 2) + O(n) & n > 1 \end{cases} T(n)={O(1)2T(n/2)+O(n)?n1n>1?

T ( n ) = O ( n log ? n ) T(n) = O(n \log{n}) T(n)=O(nlogn)

平均时间复杂性
  • 可以证明,快速排序算法在平均情况下的时间复杂性也是 O ( n log ? n ) O(n \log{n}) O(nlogn)

2.9|线性时间选择

问题描述
  • 给定线性序集中 n n n个元素和一个整数 k ( 1 ≤ k ≤ n ) k (1 \leq k \leq n) k(1kn),找出这 n n n个元素中第 k k k小的元素
随机选择算法
Python实现
import random


def partition(nums, low, high):
    pivot_index = random.randint(low, high)
    pivot = nums[pivot_index]

    # 将pivot元素移动到列表的最右边
    nums[pivot_index], nums[high] = nums[high], nums[pivot_index]

    # 通过交换操作, 将小于 pivot 的元素移动到左边, 大于 pivot 的元素移动到右边
    i = low
    for j in range(low, high):
        if nums[j] < pivot:
            nums[i], nums[j] = nums[j], nums[i]
            i += 1

    # 将 pivot 元素放置到正确的位置
    nums[i], nums[high] = nums[high], nums[i]

    return i


def quick_select(nums, low, high, k):
    if low == high:
        return nums[low]

    # 划分数组, 并获取 pivot 元素的索引
    pivot_index = partition(nums, low, high)

    j = pivot_index - low + 1
    # 如果 pivot 元素的索引等于 k, 则返回该元素
    if j == k:
        return nums[pivot_index]

    # 如果 pivot 元素的索引大于 k, 则在左侧继续查找
    elif j > k:
        return quick_select(nums, low, pivot_index - 1, k)

    # 如果 pivot 元素的索引小于 k, 则在右侧继续查找
    else:
        return quick_select(nums, pivot_index + 1, high, k - j)


def find_kth_smallest(nums, k):
    if k < 1 or k > len(nums):
        raise ValueError('Invalid value of k')

    return quick_select(nums, 0, len(nums) - 1, k)


nums = [3, 1, 5, 2, 4]
k = 2

res = find_kth_smallest(nums, k)

print(f'第 {k} 小的元素为 {res}')
时间复杂性
  • 随机选择算法在最坏情况下需要 Ω ( n 2 ) \Omega(n^{2}) Ω(n2)时间,平均情况下需要 O ( n ) O(n) O(n)时间
B F P R T BFPRT BFPRT算法
  • 如果能在线性时间内找到一个划分基准,使得按这个基准划分出的两个子数组的长度都至少为原数组长度的 ε \varepsilon ε倍( 0 < ε < 1 0< \varepsilon < 1 0<ε<1是某个常数),那么在最坏情况下用 O ( n ) O(n) O(n)时间就可以完成选择任务

    • 例如,若 ε = 9 / 10 \varepsilon = 9 / 10 ε=9/10,算法递归调用所产生的子数组的长度至少缩短 1 / 10 1 / 10 1/10,所以在最坏情况下,算法所需的计算时间 T ( n ) T(n) T(n)满足递归式 T ( n ) ≤ T ( 9 n / 10 ) + O ( n ) T(n) \leq T(9n / 10) + O(n) T(n)T(9n/10)+O(n),由此可得 T ( n ) = O ( n ) T(n) = O(n) T(n)=O(n)
  • n n n个输入元素划分成 ? n / 5 ? \left\lceil n / 5 \right\rceil ?n/5?个组,每组 5 5 5个元素(除可能有一个组不是 5 5 5个元素外),用任意一种排序算法,将每组中的元素排好序,并取出每组的中位数,共 ? n / 5 ? \left\lceil n / 5 \right\rceil ?n/5?

  • 递归调用找出这 ? n / 5 ? \left\lceil n / 5 \right\rceil ?n/5?个元素的中位数,如果 ? n / 5 ? \left\lceil n / 5 \right\rceil ?n/5?是偶数,就找它的两个中位数中较大的一个,然后以这个元素作为划分基准

  • 设所有元素互不相同,找出的基准 x x x至少比 3 ? ( n ? 5 ) / 10 ? 3 \left\lfloor (n - 5) / 10 \right\rfloor 3?(n?5)/10?个元素大,至少比 3 ? ( n ? 5 ) / 10 ? 3 \left\lfloor (n - 5) / 10 \right\rfloor 3?(n?5)/10?个元素小,当 n ≥ 75 n \geq 75 n75时, 3 ? ( n ? 5 ) / 10 ? ≥ n / 4 3 \left\lfloor (n - 5) / 10 \right\rfloor \geq n / 4 3?(n?5)/10?n/4,所以按此基准划分所得的两个子数组的长度都至少缩短 1 / 4 1 / 4 1/4

时间复杂性
  • 设对 n n n个元素的数组调用算法需要 T ( n ) T(n) T(n)时间
  • 找中位数 x x x最多用 T ( n / 5 ) T(n / 5) T(n/5)时间
  • 按照算法所选的基准 x x x进行划分所得的两个子数组分别最多有 3 n / 4 3n / 4 3n/4个元素,无论对哪一个子数组调用算法都最多用 T ( 3 n / 4 ) T(3n / 4) T(3n/4)时间

T ( n ) ≤ { C 1 , n < 75 C 2 n + T ( n / 5 ) + T ( 3 n / 4 ) , n ≥ 75 T(n) \leq \begin{cases} C_{1} , & n < 75 \\ C_{2} n + T(n / 5) + T(3n / 4) , & n \geq 75 \end{cases} T(n){C1?,C2?n+T(n/5)+T(3n/4),?n<75n75?

T ( n ) = O ( n ) T(n) = O(n) T(n)=O(n)

Python实现
import statistics


def find_median_of_medians(arr):
    # 将数组划分为大小为 5 的子数组
    subarrays = [arr[i:i + 5] for i in range(0, len(arr), 5)]

    # 计算每个子数组的中位数
    medians = [statistics.median(subarray) for subarray in subarrays]

    # 如果元素数量小于等于 5, 直接返回中位数
    if len(medians) <= 5:
        return statistics.median(medians)

    # 递归调用中位数的中位数算法
    return find_median_of_medians(medians)


def linear_time_select(arr, k):
    # 找到中位数的中位数
    median_of_medians = find_median_of_medians(arr)

    # 将数组划分为三个部分
    less = [x for x in arr if x < median_of_medians]
    equal = [x for x in arr if x == median_of_medians]
    greater = [x for x in arr if x > median_of_medians]

    # 根据划分后的数组长度选择下一步操作
    if k <= len(less):
        # 在较小的部分递归查找第 k 小元素
        return linear_time_select(less, k)
    elif k <= len(less) + len(equal):
        # 第 k 小元素等于中位数的中位数
        return median_of_medians
    else:
        # 在较大的部分递归查找第 k 小元素
        return linear_time_select(greater, k - len(less) - len(equal))


arr = [3, 1, 5, 2, 4, 9, 7, 8, 6]
k = 5

res = linear_time_select(arr, k)

print(f'第 {k} 小的元素为 {res}')

2.10|最接近点对问题

问题描述
  • 给定平面上 n n n个点,找其中的一对点,使得在 n n n个点组成的所有点对中,该点对的距离最小
一维最接近点对算法
Python实现
import sys


def closest_pair(points):
    points.sort()  # 按照横坐标排序
    min_dist = sys.maxsize  # 初始化最小距离为一个很大的数
    closest = None  # 初始化最接近点对为 None

    for i in range(len(points) - 1):
        dist = abs(points[i] - points[i + 1])  # 计算相邻点对的距离

        if dist < min_dist:
            min_dist = dist
            closest = (points[i], points[i + 1])

    return closest


points = [2, 4, 1, 5, 8, 9, 3]

res = closest_pair(points)

print(f'最接近的点对: {res}')
二维最接近点对算法
分治算法
  • 选取一垂直线 l : x = m l : x = m l:x=m m m m S S S中各点 x x x坐标的中位数,将 S S S分割为 S 1 = { ? p ∈ S ∣ x ( p ) ≤ m ? } S_{1} = \set{p \in S \mid x(p) \leq m} S1?={pSx(p)m} S 2 = { ? p ∈ S ∣ x ( p ) > m ? } S_{2} = \set{p \in S \mid x(p) > m} S2?={pSx(p)>m}
  • 递归地在 S 1 S_{1} S1? S 2 S_{2} S2?上解最接近点对问题,分别得到 S 1 S_{1} S1? S 2 S_{2} S2?中的最小距离 d 1 d_{1} d1? d 2 d_{2} d2?
  • d = min ? { ? d 1 , d 2 ? } d = \min\set{d_{1} , d_{2}} d=min{d1?,d2?},若 S S S的最接近点对 ( p , q ) (p , q) (p,q)之间的距离小于 d d d,则 p p p q q q必分属于 S 1 S_{1} S1? S 2 S_{2} S2?,设 p ∈ S 1 p \in S_{1} pS1? q ∈ S 2 q \in S_{2} qS2?,则 p p p q q q距直线 l l l的距离均小于 d d d
  • P 1 P_{1} P1? P 2 P_{2} P2?分别表示直线 l l l的左侧和右侧宽为 d d d的两个垂直长条区域,则 p ∈ P 1 p \in P_{1} pP1? q ∈ P 2 q \in P_{2} qP2?,此时 P 1 P_{1} P1?中所有点与 P 2 P_{2} P2?中所有点构成的点对均为最接近点对的候选者,在最坏情况下有 n 2 / 4 n^{2} / 4 n2/4对这样的候选者,但是对于 P 1 P_{1} P1?中任一点 p p p P 2 P_{2} P2?中最多只有 6 6 6个点与它构成最接近点对的候选者
    • 实际上对于 P 1 P_{1} P1?中任一点 p p p,若与 P 2 P_{2} P2?中的点构成最接近点对的候选者,则必有 d i s t a n c e ( p , q ) < d distance(p , q) < d distance(p,q)<d,满足这个条件的 P 2 P_{2} P2?中的点一定落在一个 d × 2 d d \times 2d d×2d的矩形 R R R
    • 可将矩形 R R R的长为 2 d 2d 2d的边 3 3 3等分,将长为 d d d的边 2 2 2等分,由此导出 6 6 6 ( d / 2 ) × ( 2 d / 3 ) (d / 2) \times (2d / 3) (d/2)×(2d/3)的矩形,矩形 R R R中最多只有 6 6 6 S S S中的点

1

  • 合并步骤中,最多只需检查 6 × n / 2 = 3 n 6 \times n / 2 = 3n 6×n/2=3n个候选者,为了确切地知道要检查哪 6 6 6个点,将 p p p P 2 P_{2} P2?中的点投影到垂直线 l l l上,能与 p p p点一起构成最接近点对候选者的 q q q p p p l l l上投影点的距离小于 d d d,且这种投影点最多只有 6 6 6个,若将 P 1 P_{1} P1? P 2 P_{2} P2?中所有 S S S中点按其 y y y坐标排好序,则对 P 1 P_{1} P1?中所有点,对排好序的点列做一次扫描,就可以找出所有最接近点对的候选者
时间复杂性

T ( n ) = { O ( 1 ) , n < 4 2 T ( n / 2 ) + O ( n ) , n ≥ 4 T(n) = \begin{cases} O(1) , & n < 4 \\ 2 T(n / 2) + O(n) , & n \geq 4 \end{cases} T(n)={O(1),2T(n/2)+O(n),?n<4n4?

T ( n ) = O ( n log ? n ) T(n) = O(n \log{n}) T(n)=O(nlogn)

Python实现
import math


# 计算两点之间的欧几里德距离
def dist(p1, p2):
    return math.sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2)


# 分治法求解最接近点对问题
def closest_pair(points):
    # 如果点集中的点个数小于等于 3 个, 直接计算并返回最小距离对
    if len(points) <= 3:
        min_dist = float('inf')
        min_pair = None

        for i in range(len(points)):
            for j in range(i + 1, len(points)):
                d = dist(points[i], points[j])

                if d < min_dist:
                    min_dist = d
                    min_pair = (points[i], points[j])

        return min_pair

    # 将点集按照 x 坐标排序
    sorted_points = sorted(points, key=lambda p: p[0])

    # 将点集分成左右两部分
    mid = len(sorted_points) // 2

    left_points = sorted_points[:mid]
    right_points = sorted_points[mid:]

    # 递归求解左右两部分的最接近点对
    left_min_pair = closest_pair(left_points)
    right_min_pair = closest_pair(right_points)

    # 取左右两部分最接近点对的最小距离
    if left_min_pair is None:
        min_dist = dist(right_min_pair[0], right_min_pair[1])
        min_pair = right_min_pair
    elif right_min_pair is None:
        min_dist = dist(left_min_pair[0], left_min_pair[1])
        min_pair = left_min_pair
    else:
        left_dist = dist(left_min_pair[0], left_min_pair[1])
        right_dist = dist(right_min_pair[0], right_min_pair[1])

        if left_dist <= right_dist:
            min_dist = left_dist
            min_pair = left_min_pair
        else:
            min_dist = right_dist
            min_pair = right_min_pair

    # 在横跨左右两部分的点中寻找更近的点对
    mid_x = sorted_points[mid][0]
    strip = []

    # 将点集按照 y 坐标排序
    sorted_points = sorted(points, key=lambda p: p[1])

    for point in sorted_points:
        if abs(point[0] - mid_x) < min_dist:
            strip.append(point)

    for i in range(len(strip)):
        for j in range(i + 1, min(i + 7, len(strip))):
            d = dist(strip[i], strip[j])

            if d < min_dist:
                min_dist = d
                min_pair = (strip[i], strip[j])

    return min_dist, min_pair


points = [(2, 3), (12, 30), (40, 50), (5, 1), (12, 10), (3, 4)]

min_dist, min_pair = closest_pair(points)

print(f'最接近的点对为: {min_pair}, 点对距离为 {min_dist}')

2.11|循环赛日程表

问题描述
  • 设有 n = 2 k n = 2^{k} n=2k个运动员要进行网球循环赛,设计一个满足以下要求的比赛日程表
    • 每个选手必须与其他 n ? 1 n - 1 n?1个选手各赛一次
    • 每个选手一天只能赛一次
    • 循环赛一共进行 n ? 1 n - 1 n?1
分治算法
  • n n n个选手的比赛日程表可以通过 n / 2 n / 2 n/2个选手的比赛日程表决定,递归调用,直到只剩下两个选手,比赛日程表的制定就变得简单了
示例
  • 8 8 8个选手的比赛日程表
  • 先制定 2 2 2个选手的比赛日程表,如下图所示

2

  • 有了最基本情况后就可以逐层返回,得到 4 4 4个选手的比赛日程表,如下图所示

3

  • 最后得到 8 8 8个选手的比赛日程表,如下图所示

4

  • 其中第 1 1 1列表示 8 8 8个选手,第 i ( 2 ≤ i ≤ 8 ) i (2 \leq i \leq 8) i(2i8)列表示第 i ? 1 i - 1 i?1天每个选手的对手
Python实现
def generate_schedule(k):
    n = 1
    for i in range(1, k + 1):
        n *= 2

    schedule = [[0] * (n + 1) for _ in range(n + 1)]

    for i in range(1, n + 1):
        schedule[1][i] = i

    m = 1
    for s in range(1, k + 1):
        n //= 2

        for t in range(1, n + 1):
            for i in range(m + 1, 2 * m + 1):
                for j in range(m + 1, 2 * m + 1):
                    schedule[i][j + (t - 1) * m * 2] = schedule[i - m][j + (t - 1) * m * 2 - m]
                    schedule[i][j + (t - 1) * m * 2 - m] = schedule[i - m][j + (t - 1) * m * 2]

        m *= 2

    return schedule


k = 3

schedule = generate_schedule(k)

for item in schedule:
    print(item)
无运动员数量约束循环赛日程表算法
  • 如果队伍数为奇数,则添加一个虚拟队伍来凑成偶数
  • 固定第一支队伍的位置,其他队伍按顺序循环移动
示例
  • 5 5 5个选手的比赛日程表

  • 选手数为奇数,首先添加一个虚拟队伍 6 6 6来凑成偶数,如下图所示

5

  • 1 1 1轮竞争队伍安排,其中与虚拟队伍 6 6 6比赛即为轮空,如下图所示

6

  • 固定队伍 1 1 1的位置,其他队伍按顺序循环移动,得到第 2 2 2轮竞争队伍安排,如下图所示

7

  • 3 3 3 5 5 5轮以此类推

8

Python实现
def generate_schedule(num_teams):
    # 如果队伍数为奇数, 添加一个虚拟队伍来凑成偶数
    if num_teams % 2 != 0:
        num_teams += 1

    num_rounds = num_teams - 1  # 总轮数
    half_teams = num_teams // 2  # 每轮比赛的队伍数

    teams = list(range(1, num_teams + 1))

    schedule = []
    for round in range(num_rounds):
        matches = []

        for i in range(half_teams):
            match = (teams[i], teams[num_teams - i - 1])
            matches.append(match)

        schedule.append(matches)

        # 重新排列队伍, 固定第一支队伍, 其他队伍按顺序循环移动
        teams.insert(1, teams.pop())

    return schedule


num_teams = 8

schedule = generate_schedule(num_teams)

round_num = 1
for matches in schedule:
    print(f'Round {round_num}:')

    for match in matches:
        print(f'Team {match[0]} vs Team {match[1]}')

    print()

    round_num += 1

文章来源:https://blog.csdn.net/from__2023_11_28/article/details/134939896
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。