Algorithm + Data Structures = Programs

本文最后更新于 2024年10月5日 上午

弃坑了,转战Jeff的《Algorithms》

第三章 分治算法

本章内容汇总

第一节:分治策略的设计思想

分治策略(Divide and Conquer)

  1. 将原始问题划分或者归结为规模较小的子问题
  2. 迭归或逐代求解每个子问题
  3. 将子问题的解综合得到原问题的解

Remark

  1. 子问题与原始问题性质完全一样
  2. 子问题之间可彼此独立地求解
  3. 迭归停止时子问题可直接求解

二分归并排序

这段代码实现了二分归并排序算法。它将输入数组 A 分成两半递归地进行排序,然后再将排好序的两个子数组合并成一个有序数组。

二分归并排序设计思想
  1. 将原问题归结为规模为 n2\frac{n}{2} 的 2 个子问题。
  2. 继续划分,将原问题归结为规模为 n4\frac{n}{4} 的 4 个子问题,继续上述操作,当子问题规模为 1 时,划分结束。
  3. 从规模为 1 到 n2\frac{n}{2} ,逐层合并子问题的解,得到原问题的解。
二分归并排序时间复杂度分析

假设 n 为 2 的幂,则二分归并排序最坏情况下时间复杂度为:

W(n)=2W(n2)+n1W(1)=0\begin{align*} W(n) & = 2W(\frac{n}{2}) + n - 1 \\ W(1) & = 0 \end{align*}

解得:W(n)=nlognn+1W(n) = n \log n - n + 1

Hanoi\mathrm{Hanoi} 塔问题

算法 Hanoi(A,C,n)\mathrm{Hanoi}(A, C, n) // nn 个盘子从 AACC

  1. if n=1n=1 then move (A,C)(A, C) // 1 个盘子从 AACC
  2. else Hanoi(A,B,n1)\mathrm{Hanoi}(A, B, n-1)
  3. move (A,C)\qquad \text{move }(A, C)
  4. Hanoi(B,C,n1)\qquad \mathrm{Hanoi}(B, C, n-1)
Hanoi\mathrm{Hanoi} 塔问题的时间复杂度分析

nn 个盘子的移动次数为 T(n)T(n),则有

T(1)=1T(n)=2T(n1)+1\begin{align*} T(1) & = 1 \\ T(n) & = 2T(n-1) + 1 \end{align*}

解得,T(n)=2n1T(n) = 2^n - 1

Hanoi\mathrm{Hanoi} 塔问题的算法设计思想
  1. 将原问题归结为规模为 n1n-1 的 2 个子问题。
  2. 继续归约,将原问题归结为规模为 n2n-2 的 4 个子问题,继续上述操作,当子问题规模为 1 时,归约过程结束。
  3. 从规模为 1 到 n1n-1 ,逐层合并子问题的解,得到原问题的解。

小结

通过几个例子展示分治算法的特点:

  • 将原问题归结为规模小的子问题,子问题与原问题具有相同的性质。
  • 子问题规模足够小时可直接求解。
  • 算法可以递归也可以迭代实现。
  • 算法的分析方法: 递推方程。

第二节:分治算法的一般性描述与分析方法

分治算法 Divide-and-Conquer(P)\textrm{Divide-and-Conquer}(P) 的一般性描述

  1. if Pc|P| ≤ c then S(P)S(P) // 如果问题规模足够,直接求解
  2. divide PP into P1,P2,,PkP1, P2, \cdots, Pk // 划分子问题
  3. for i1i \leftarrow 1 to kk
  4. yiDivide-and-Conquer(Pi)\qquad y_i \leftarrow \textrm{Divide-and-Conquer}(P_i) // 递归求解子问题
  5. Return Merge(y1,y2,,yk)Merge (y_1, y_2, \cdots, y_k) // 合并子问题的解

设计要点

  1. 原问题可以划分或者归约为规模较小的子问题
    1. 子问题与原问题具有相同的性质
    2. 子问题的求解彼此独立
    3. 划分子问题的规模尽可能均衡
  2. 子问题规模足够小时可直接求解
  3. 子问题的解综合得到原问题的解
  4. 算法实现: 递归或迭代

分治算法时间分析

时间复杂度函数的递推方程:

W(n)=W(P1)+W(P2)+...+W(Pk)+f(n)W(c)=C\begin{align*} W(n) & = W(|P1|) + W(|P2|) + ... + W(|Pk|) + f(n) \\ W(c) & = C \end{align*}

其中:

  • P1,P2,,PkP1, P2, \cdots, Pk 为划分后的子问题
  • f(n)f(n) 为划分子问题以及将子问题解综合得到原问题解的总工作量
  • 规模为 cc 的最小子问题的工作量为 CC

两类常见的递推方程

f(n)=i=1kaif(ni)+g(n)f(n)=af(n/b)+d(n)\begin{align} f(n) & = \sum_{i=1}^k a_i f(n-i) + g(n) \\ f(n) & = a * f(n/b) + d(n) \end{align}

求解方法:

  • 方程 1:迭代法、递归树
  • 方程 2:迭代法、换元法、递归树、主定理

方程2: T(n)=aT(n/b)+d(n)T(n)=a T(n / b)+d(n)

d(n)d(n) 为常数时:

T(n)={O(nlogba)a1O(logn)a=1T(n)=\left\{\begin{array}{ll} O\left(n^{\log _b a}\right) & a \neq 1 \\ O(\log n) & a=1 \end{array}\right.

d(n)=cnd(n)=c n 时:

T(n)={O(n)a<bO(nlogn)a=bO(nlogba)a>bT(n)=\left\{\begin{array}{ll} O(n) & a<b \\ O(n \log n) & a=b \\ O\left(n^{\log _{b} a}\right) & a>b \end{array}\right.

小结

小结

第三节:芯片测试

一次芯片测试
A 报告 B 报告 结论
B 是好的 A 是好的 A、B 都好或 A、B 都坏
B 是好的 A 是坏的 至少一片是坏的
B 是坏的 A 是好的 至少一片是坏的
B 是坏的 A 是坏的 至少一片是坏的

问题

输入:
nn 片芯片,其中好芯片至少比坏芯片多 11 片。
问题:
设计一种测试方法,通过芯片测试,从 nn 片芯片中挑出 11 片好芯片。
要求:
使用最少的测试次数。

算法设计

判定芯片A的好坏

问题:给定芯片 A,判定 A 的好坏
方法:用其他 n1n-1 片芯片对 A 测试

举例:
n=7n=7 时, 好芯片数 4\geqslant 4
A 好, 6 个报告中至少 3 个报告"好"
A 坏, 6 个报告中至少 4 个报告"坏"

n 是奇数: 好芯片数 (n+1)/2\geqslant (n+1)/2
A 好, 至少有 (n1)/2(n-1)/2 个报告"好"
A 坏, 至少有 (n+1)/2(n+1)/2 个报告"坏"

n=8n=8 时, 好芯片数 5\geqslant 5
A 好, 7 个报告中至少 4 个报告"好"
A 坏, 7 个报告中至少 5 个报告"坏"

n 是偶数: 好芯片数 n/2+1\geqslant n/2 + 1
A 好, 至少有 n/2n/2 个报告"好"
A 坏, 至少有 n/2+1n/2+1 个报告"坏"

结论:
至少一半报告"好", A 是好芯片,
超过一半报告"坏", A 是坏芯片。

暴力算法

测试方法:
任取 1 片测试, 如果是好芯片, 测试结束; 如果是坏芯片, 再从剩下芯片中任取 1 片测试, 直到得到 1 片好芯片。

时间估计:
第 1 片坏芯片, 最多测试 n-2 次,
第 2 片坏芯片, 最多测试 n-3 次,
\cdots
总计 Θ(n2)\Theta(n²)

分治算法

偶数情形

假设 n 为偶数, 将 n 片芯片两两一组做测试。剩下的芯片构成子问题,进入下一轮分组淘汰。

淘汰规则:
“好, 好” \Rightarrow 任留 1 片, 进入下轮
其他情况 \Rightarrow 全部淘汰

终止条件: n3n \leqslant 3

  1. 3 片芯片, 1 次测试可得到好芯片。(随意选取两个再测试一次,若报告全好,则证明这两个全好;否则,剩下那个必定是好)
  2. 1 或 2 片芯片, 不再需要测试。(剩下的都是好芯片)
偶数情形
奇数情形
奇数情形
伪代码
伪代码
时间复杂度分析

设输入规模为 nn, 每轮淘汰后, 芯片数至少减半,测试次数(含轮空处理): O(n)O(n)

时间复杂度:

W(n)=W(n/2)+O(n)W(3)=1,W(2)=W(1)=0\begin{align*} W(n) &= W(n/2) + O(n) \\ W(3) &= 1, \quad W(2) = W(1) = 0 \end{align*}

解得:

W(n)=O(n)W(n) = O(n)

小结

小结

第四节:快速排序

基本思想

基本思想

伪代码

快排伪代码 划分过程

实例

划分实例

时间复杂度分析

最坏情况:

W(n)=W(n1)+n1W(1)=0W(n)=n(n1)/2\begin{align*} W(n) & = W(n-1) + n-1 \\ W(1) & = 0 \\ W(n) & = n(n-1)/2 \end{align*}

最好划分:

T(n)=2T(n/2)+n1T(1)=0T(n)=Θ(nlogn)\begin{align*} T(n) & = 2 T(n/2) + n-1 \\ T(1) & = 0 \\ T(n) & = \Theta(n \log n) \end{align*}

均衡划分

均衡划分的时间复杂度 递归树

平均时间复杂度

平均时间复杂度1 平均时间复杂度2

小结

快速排序算法

  1. 分治策略
  2. 子问题划分是由首元素决定
  3. 最坏情况下时间复杂度为 O(n2)O(n²)
  4. 平均情况下时间复杂度为 O(nlogn)O(n \log n)

第五节:幂乘算法及其应用

输入: aa 为给定实数, nn 为自然数
输出: ana^n

传统算法:
顺序相乘:an=(((aa)a)a)aa^n = (\cdots ((a a)a)a)\cdots a

时间复杂度: Θ(n)\Theta(n)

分治算法————划分

划分

算法分析

以乘法作为基本运算

  • 子问题规模: 不超过 n/2n/2
  • 两个规模近似 n/2n/2 的子问题完全一样, 只要计算 1 次

W(n)=W(n/2)+Θ(1)W(n)=Θ(logn)\begin{align*} W(n) & = W(n/2) + \Theta(1) \\ W(n) & = \Theta( \log n) \end{align*}

实例:Fibonacci 数列

传统算法

斐波那契数列

分治算法

定理1 定理1(续)
幂乘算法的复杂度

小结

  • 分治算法的例子——幂乘算法
  • 幂乘算法的应用
    • 计算 Fibonacci 数列
    • 通常算法 O(n)O(n), 分治算法为 O(logn)O(\log n)

第六节:改进分治算法的途径

改进方法一:减少子问题数

分治算法的时间复杂度方程:

W(n)=aW(n/b)+d(n)W(n) = aW(n/b) + d(n)

其中:

  • aa: 子问题数
  • n/bn/b: 子问题规模
  • d(n)d(n): 划分与综合工作量

aa 较大, bb 较小, d(n)d(n) 不大时, 方程的解为:

W(n)=Θ(nlogba)W(n) = \Theta(n^{log_b a})

注意事项:

  • 减少 aa 是降低函数 W(n)W(n) 的阶的途径
  • 利用子问题的依赖关系, 使某些子问题的解通过其他子问题的解而得到。

整数位乘问题

整数位乘问题

因此,简单的分治并不一定能简化时间复杂度

时间复杂度

矩阵相乘问题

矩阵相乘 分治法
Strassen-1 Strassen-2 Strassen-1

小结

小结

改进方法二:增加预处理

平面点对问题

平面点对-1 平面点对-2
伪代码
伪代码-1 跨边界处理 伪代码-2
增加预处理
原算法 递归中的拆分 改进时间复杂度

小结

小结

算法设计与分析Ch03
http://dbqdss.github.io/2024/08/05/算法设计与分析/算法设计与分析Ch03/
作者
DBQDSS
发布于
2024年8月5日
许可协议