《计算机算法设计与分析(第5版)》笔记

文章目录

    • @[toc]
    • 第一章:算法概述
      • 1.1|算法与程序
        • 算法
          • 性质
        • 程序
      • 1.2|算法复杂性分析
        • 算法复杂性
        • 时间复杂性
          • 最坏情况下的时间复杂性
          • 最好情况下的时间复杂性
          • 平均情况下的时间复杂性
        • 渐进复杂性
        • O O O表示法、 Ω \Omega Ω表示法、 θ \theta θ表示法、 o o o表示法
          • O O O表示法
            • 运算规则
          • Ω \Omega Ω表示法
          • θ \theta θ表示法
          • o o o表示法
      • 1.3|`NP`完全性理论
        • 判定形式的旅行售货员问题
        • `P`类问题
        • `NP`类问题
        • `NP`完全问题——`NPC`问题
          • 典型的`NPC`问题
    • 第二章:递归与分治策略
      • 2.1|递归的概念
        • F i b o n a c c i Fibonacci Fibonacci数列非递归定义
        • A c k e r m a n Ackerman Ackerman函数
          • 递归方程
          • `Python`实现
        • 排列问题
          • 问题描述
          • 分治算法
          • `Python`实现
        • 整数划分问题
          • 问题描述
          • 分治算法
          • `Python`实现
        • `Hanoi`塔问题
          • 问题描述
          • `Python`实现
      • 2.2|分治法的基本思想
        • 分治法时间复杂性
      • 2.3|二分搜索技术
        • 问题描述
        • `Python`实现
        • 时间复杂性
      • 2.4|大整数的乘法
        • 问题描述
        • 基础算法
          • 时间复杂性
        • 优化算法
          • 时间复杂性
        • `Python`实现
      • 2.5|`Strassen`矩阵乘法
        • 问题描述
        • 基础算法
          • 时间复杂性
        • `Strassen`算法
          • 时间复杂性
        • 问题时间复杂性
        • `Python`实现
      • 2.6|棋盘覆盖
        • 问题描述
        • 分治算法
        • 时间复杂性
        • `Python`实现
      • 2.7|合并排序
        • 递归算法
          • `Python`实现
          • 时间复杂性
        • 非递归算法
          • `Python`实现
        • 自然合并排序
      • 2.8|快速排序
        • `Python`实现
        • 时间复杂性
          • 最坏时间复杂性
          • 最好时间复杂性
          • 平均时间复杂性
      • 2.9|线性时间选择
        • 问题描述
        • 随机选择算法
          • `Python`实现
          • 时间复杂性
        • B F P R T BFPRT BFPRT算法
          • 时间复杂性
          • `Python`实现
      • 2.10|最接近点对问题
        • 问题描述
        • 一维最接近点对算法
          • `Python`实现
        • 二维最接近点对算法
          • 分治算法
          • 时间复杂性
          • `Python`实现
      • 2.11|循环赛日程表
        • 问题描述
        • `Python`实现
        • 无运动员数量约束循环赛日程表算法`Python`实现

第一章:算法概述


1.1|算法与程序

算法
性质
  • 确定性:组成算法的每条指令是清晰的,无歧义的

  • 有限性:算法中每条指令的执行次数是有限的,执行每条指令的时间也是有限的

程序
  • 程序可以不满足有限性

1.2|算法复杂性分析

算法复杂性
  • 分别用 N N N I I I A A A表示算法要解的问题的规模、算法的输入和算法本身,用 C C C表示复杂性,有 C = F ( N , I , A ) C = F(N , I , A) C=F(N,I,A)

  • 通常 A A A隐含在复杂性函数名当中

时间复杂性
  • T ( N , I ) T(N , I) T(N,I)是算法在一台抽象的计算机上运行所需要的时间,设此抽象的计算机提供的元运算有 k k k种,分别记为 O 1 O_{1} O1 O 2 O_{2} O2 ⋯ \cdots O k O_{k} Ok,每执行一次这些元运算所需要时间分别为 t 1 t_{1} t1 t 2 t_{2} t2 ⋯ \cdots t k t_{k} tk
  • 对于给定的算法 A A A,经统计,用到元运算 O i O_{i} Oi的次数为 e i ( i = 1 , 2 , ⋯   , k ) e_{i} (i = 1 , 2 , \cdots , k) ei(i=1,2,,k),对于每个 i ( 1 ≤ i ≤ k ) i (1 \leq i \leq k) i(1ik) e i e_{i} ei N N N I I I的函数,即 e i = e i ( N , I ) e_{i} = e_{i}(N , I) ei=ei(N,I),因此有 T ( N , I ) = ∑ i = 1 k t i e i ( N , I ) T(N , I) = \displaystyle\sum\limits_{i = 1}^{k}{t_{i} e_{i}(N , I)} T(N,I)=i=1ktiei(N,I)
最坏情况下的时间复杂性
  • T max ⁡ ( N ) = max ⁡ I ∈ D N T ( N , I ) = max ⁡ I ∈ D N ∑ i = 1 k t i e i ( N , I ) = ∑ i = 1 k t i e i ( N , I ∗ ) = T ( N , I ∗ ) T_{\max}(N) = \max\limits_{I \in D_{N}}{T(N , I)} = \max\limits_{I \in D_{N}}{\displaystyle\sum\limits_{i = 1}^{k}{t_{i} e_{i}(N , I)}} = \displaystyle\sum\limits_{i = 1}^{k}{t_{i} e_{i}(N , I^{*})} = T(N , I^{*}) Tmax(N)=IDNmaxT(N,I)=IDNmaxi=1ktiei(N,I)=i=1ktiei(N,I)=T(N,I)

  • D N D_{N} DN是规模为 N N N的合法输入的集合, I ∗ I^{*} I D N D_{N} DN中使 T ( N , I ∗ ) T(N , I^{*}) T(N,I)达到 T max ⁡ ( N ) T_{\max}(N) Tmax(N)的合法输入

最好情况下的时间复杂性
  • T min ⁡ ( N ) = min ⁡ I ∈ D N T ( N , I ) = min ⁡ I ∈ D N ∑ i = 1 k t i e i ( N , I ) = ∑ i = 1 k t i e i ( N , I ~ ) = T ( N , I ~ ) T_{\min}(N) = \min\limits_{I \in D_{N}}{T(N , I)} = \displaystyle\min\limits_{I \in D_{N}}{\displaystyle\sum\limits_{i = 1}^{k}{t_{i} e_{i}(N , I)}} = \displaystyle\sum\limits_{i = 1}^{k}{t_{i} e_{i}(N , \widetilde{I})} = T(N , \widetilde{I}) Tmin(N)=IDNminT(N,I)=IDNmini=1ktiei(N,I)=i=1ktiei(N,I )=T(N,I )

  • I ~ \widetilde{I} I D N D_{N} DN中使 T ( N , I ~ ) T(N , \widetilde{I}) T(N,I )达到 T min ⁡ ( N ) T_{\min}(N) Tmin(N)的合法输入

平均情况下的时间复杂性
  • T a v g ( N ) = ∑ I ∈ D N P ( I ) T ( N , I ) = ∑ I ∈ D N P ( I ) ∑ i = 1 k t i e i ( N , I ) T_{\mathrm{avg}}(N) = \displaystyle\sum\limits_{I \in D_{N}}{P(I) T(N , I)} = \displaystyle\sum\limits_{I \in D_{N}}{P(I)} \displaystyle\sum\limits_{i = 1}^{k}{t_{i} e_{i}(N , I)} Tavg(N)=IDNP(I)T(N,I)=IDNP(I)i=1ktiei(N,I)

  • P ( I ) P(I) P(I)是在算法的应用中出现输入 I I I的概率

渐进复杂性
  • N N N单调增大且趋于 ∞ \infty 时, T ( N ) T(N) T(N)一般也将单调增大且趋于 ∞ \infty

  • 对于 T ( N ) T(N) T(N),如果存在 T ~ ( N ) \widetilde{T}(N) T (N),当 N → ∞ N \rightarrow \infty N时,使得 ( T ( N ) − T ~ ( N ) ) / T ( N ) → 0 (T(N) - \widetilde{T}(N)) / T(N) \rightarrow 0 (T(N)T (N))/T(N)0,就说 T ~ ( N ) \widetilde{T}(N) T (N) T ( N ) T(N) T(N) N → ∞ N \rightarrow \infty N时的渐进性态,或称 T ~ ( N ) \widetilde{T}(N) T (N)为算法 A A A N → ∞ N \rightarrow \infty N的渐进复杂性

  • T ~ ( N ) \widetilde{T}(N) T (N) T ( N ) T(N) T(N)中略去低阶项留下的主项,比 T ( N ) T(N) T(N)简单

O O O表示法、 Ω \Omega Ω表示法、 θ \theta θ表示法、 o o o表示法
O O O表示法
  • f ( N ) f(N) f(N) g ( N ) g(N) g(N)是定义在正数集上的正函数,如果存在正的常数 C C C和自然数 N 0 N_{0} N0,使得当 N ≥ N 0 N \geq N_{0} NN0时有 f ( N ) ≤ C g ( N ) f(N) \leq C g(N) f(N)Cg(N),则称函数 f ( N ) f(N) f(N) N N N充分大时上有界,且 g ( N ) g(N) g(N)是它的一个上界,记为 f ( N ) = O ( g ( N ) ) f(N) = O(g(N)) f(N)=O(g(N))
运算规则
  • O ( f ) + O ( g ) = O ( max ⁡ ( f , g ) ) O(f) + O(g) = O(\max(f , g)) O(f)+O(g)=O(max(f,g))

    • 证明
      • F ( N ) = O ( f ) F(N) = O(f) F(N)=O(f),存在正常数 C 1 C_{1} C1和自然数 N 1 N_{1} N1,使得对所有的 N ≥ N 1 N \geq N_{1} NN1,有 F ( N ) ≤ C 1 f ( N ) F(N) \leq C_{1} f(N) F(N)C1f(N)
      • G ( N ) = O ( g ) G(N) = O(g) G(N)=O(g),存在正常数 C 2 C_{2} C2和自然数 N 2 N_{2} N2,使得对所有的 N ≥ N 2 N \geq N_{2} NN2,有 G ( N ) ≤ C 2 g ( N ) G(N) \leq C_{2} g(N) G(N)C2g(N)
      • C 3 = max ⁡ {   C 1 , C 2   } C_{3} = \max\set{C_{1} , C_{2}} C3=max{C1,C2} N 3 = max ⁡ {   N 1 , N 2   } N_{3} = \max\set{N_{1} , N_{2}} N3=max{N1,N2} h ( N ) = max ⁡ {   f , g   } h(N) = \max\set{f , g} h(N)=max{f,g},则对所有的 N ≥ N 3 N \geq N_{3} NN3,有 F ( N ) ≤ C 1 f ( N ) ≤ C 1 h ( N ) ≤ C 3 h ( N ) F(N) \leq C_{1} f(N) \leq C_{1} h(N) \leq C_{3} h(N) F(N)C1f(N)C1h(N)C3h(N) G ( N ) ≤ C 2 g ( N ) ≤ C 2 h ( N ) ≤ C 3 h ( N ) G(N) \leq C_{2} g(N) \leq C_{2} h(N) \leq C_{3} h (N) G(N)C2g(N)C2h(N)C3h(N)
      • O ( f ) + O ( g ) = F ( N ) + G ( N ) ≤ C 3 h ( N ) + C 3 h ( N ) = 2 C 3 h ( N ) = O ( h ) = O ( max ⁡ ( f , g ) ) O(f) + O(g) = F(N) + G(N) \leq C_{3} h(N) + C_{3} h(N) = 2 C_{3} h(N) = O(h) = O(\max(f , g)) O(f)+O(g)=F(N)+G(N)C3h(N)+C3h(N)=2C3h(N)=O(h)=O(max(f,g))
  • O ( f ) + O ( g ) = O ( f + g ) O(f) + O(g) = O(f + g) O(f)+O(g)=O(f+g)

  • O ( f ) O ( g ) = O ( f g ) O(f) O(g) = O(fg) O(f)O(g)=O(fg)

  • 如果 g ( N ) = O ( f ( N ) ) g(N) = O(f(N)) g(N)=O(f(N)),则 O ( f ) + O ( g ) = O ( f ) O(f) + O(g) = O(f) O(f)+O(g)=O(f)

Ω \Omega Ω表示法
  • 如果存在正的常数 C C C和自然数 N 0 N_{0} N0,使得当 N ≥ N 0 N \geq N_{0} NN0时有 f ( N ) ≥ C g ( N ) f(N) \geq C g(N) f(N)Cg(N),则称函数 f ( N ) f(N) f(N) N N N充分大时下有界,且 g ( N ) g(N) g(N)是它的一个下界,记为 f ( N ) = Ω ( g ( N ) ) f(N) = \Omega(g(N)) f(N)=Ω(g(N))

  • f ( N ) f(N) f(N)对自然数的不同无穷子集有不同的表达式,且有不同的阶时,不能很好地刻画 f ( N ) f(N) f(N)的下界,例如 f ( N ) = { 100 , N 为正偶数 6 N 2 , N 为正奇数 f(N) = \begin{cases} 100 , & N 为正偶数 \\ 6 N^{2} , & N 为正奇数 \end{cases} f(N)={100,6N2,N为正偶数N为正奇数,按上述定义,只能得到 f ( N ) = Ω ( 1 ) f(N) = \Omega(1) f(N)=Ω(1),这是一个平凡的下界,对算法分析没有什么价值

θ \theta θ表示法
  • 定义 f ( N ) = θ ( g ( N ) ) f(N) = \theta(g(N)) f(N)=θ(g(N))当且仅当 f ( N ) = O ( g ( N ) ) f(N) = O(g(N)) f(N)=O(g(N)) f ( N ) = Ω ( g ( N ) ) f(N) = \Omega(g(N)) f(N)=Ω(g(N)),称 f ( N ) f(N) f(N) g ( N ) g(N) g(N)同阶
o o o表示法
  • 如果对于任意给定的 ε > 0 \varepsilon > 0 ε>0,都存在正整数 N 0 N_{0} N0,使得当 N ≥ N 0 N \geq N_{0} NN0时有 f ( N ) / g ( N ) < ε f(N) / g(N) < \varepsilon f(N)/g(N)<ε,则称函数 f ( N ) f(N) f(N) N N N充分大时的阶比 g ( N ) g(N) g(N)低,记为 f ( N ) = o ( g ( N ) ) f(N) = o(g(N)) f(N)=o(g(N))

1.3|NP完全性理论

判定形式的旅行售货员问题
  • 对于给定的带权图 G = ( V , E ) G = (V , E) G=(V,E)和一个正数 d d d,判定图 G G G中是否存在总费用不超过 d d d的周游路线
P类问题
  • 所有可以在多项式时间内求解的判定问题构成P类问题
NP类问题
  • 非确定性算法将问题求解分为猜测和验证两个阶段
    • 猜测阶段是非确定性的
    • 验证阶段是确定性的
  • 设算法 A A A是解一个判定问题 Q Q Q的非确定性算法,如果算法 A A A的验证阶段可以在多项式时间内完成,则称算法 A A A是一个多项式时间非确定性算法,也称问题 Q Q Q是非确定性多项式时间可解的
  • 所有非确定性多项式时间可解的判定问题构成NP类问题
NP完全问题——NPC问题
  • 性质:如果一个NPC问题能在多项式时间内得到解决,那么NP中的每个问题都可以在多项式时间内求解
  • 第一个NPC问题是布尔表达式的可满足性问题,即 C o o k Cook Cook定理:布尔表达式的可满足性问题 S A T SAT SATNP完全的
  • 对于任何问题 Q Q Q,只要能证明 Q ∈ N P Q \in NP QNP,而且可以在多项式时间内将 S A T SAT SAT变换为问题 Q Q Q,便有 Q ∈ N P C Q \in NPC QNPC
  • NP完全问题树中任一结点表示的问题可以在多项式时间内变换为它的任一后裔节点表示的问题
典型的NPC问题
  • 合取范式的可满足性问题 C N F − S A T CNF-SAT CNFSAT

  • 三元合取范式的可满足性问题 3 − S A T 3-SAT 3SAT

  • 团问题 C L I Q U E CLIQUE CLIQUE

  • 顶点覆盖问题 V E R T E X − C O V E R VERTEX-COVER VERTEXCOVER

  • 子集和问题 S U B S E T − S U M SUBSET-SUM SUBSETSUM

  • 哈密顿回路问题 H A M − C Y C L E HAM-CYCLE HAMCYCLE

  • 旅行售货员问题 T S P TSP TSP


第二章:递归与分治策略


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(215 )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(n1,m),m1)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(n1,1),0)=A(n1,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(n1,2),1)=2A(n1,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(n1n2nk1,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,n1),q(n,m1)+q(nm,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)=nlogmk+j=0logmn1kjf(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+((AB)(DC)+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=1naikbkj
  • 每计算 C C C的一个元素 c i j c_{ij} cij,需要做 n n n次乘法和 n − 1 n - 1 n1次加法,求出矩阵 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} C11C21C12C22 = A11A21A12A22 B11B21B12B22

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=A11B11+A12B21C12=A11B12+A12B22C21=A21B11+A22B21C22=A21B12+A22B22

时间复杂性
  • 计算 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(B12B22)M2=(A11+A12)B22M3=(A21+A22)B11M4=A22(B21B11)M5=(A11+A22)(B11+B22)M6=(A12A22)(B21+B22)M7=(A11A21)(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+M4M2+M6C12=M1+M2C21=M3+M4C22=M5+M1M3M7

时间复杂性
  • 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} 2k1×2k1子棋盘,特殊方格必位于 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(k1)+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 (4k1)/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 n1个元素和 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(n1)+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(n5)/10个元素大,至少比 3 ⌊ ( n − 5 ) / 10 ⌋ 3 \left\lfloor (n - 5) / 10 \right\rfloor 3(n5)/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(n5)/10n/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,C2n+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('最接近的点对:', 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中的点
  • 合并步骤中,最多只需检查 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 n1个选手各赛一次
    • 每个选手一天只能赛一次
    • 循环赛一共进行 n − 1 n - 1 n1
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)
无运动员数量约束循环赛日程表算法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

你可能感兴趣的:(算法,算法,笔记,Python)