算法设计 期末复习笔记
基于 Lecture 1-7, 作业与考试热点整理
考试题型: 10 选择(20分) + 10 判断(10分) + 4 计算 + 2 算法设计
占位图

目录
- 算法基础与复杂度分析
- 排序与划分算法
- 动态规划 (DP)
- 最短路径算法
- 最小生成树 (MST)
- 最大流 (Maximum Flow)
- 二分图匹配 (Bipartite Matching)
- 线性规划 (Linear Programming)
- 附:考试热点速查表
1. 算法基础与复杂度分析
1.1 渐近符号 (Asymptotic Notation)
| 符号 | 含义 | 数学定义 | 类比 |
|---|---|---|---|
| O (大O) | 上界 (最坏情况) | 0 ≤ f(n) ≤ c·g(n), n ≥ n₀ | ≤ |
| Ω (大Ω) | 下界 (最好情况) | 0 ≤ c·g(n) ≤ f(n), n ≥ n₀ | ≥ |
| Θ (大Θ) | 紧确界 | c₁·g(n) ≤ f(n) ≤ c₂·g(n), n ≥ n₀ | = |
判断技巧:
- O 表示"不会比...差"(最坏情况的上界)
- Ω 表示"至少和...一样好"(最好情况的下界)
- Θ 表示"和...同阶"
1.2 ⭐ Master Theorem (主定理) --- 唯一考查时间复杂度的方法
求解递推式 T(n) = aT(n/b) + f(n), 其中 a ≥ 1, b > 1
计算 log_b a, 比较 f(n) 与 n^(log_b a):
| Case | 条件 | 结论 |
|---|---|---|
| Case 1 | f(n) = O(n^(log_b a - ε)), ε > 0 | T(n) = Θ(n^(log_b a)) |
| Case 2 | f(n) = Θ(n^(log_b a) · log^k n), k ≥ 0 | T(n) = Θ(n^(log_b a) · log^(k+1) n) |
| Case 3 | f(n) = Ω(n^(log_b a + ε)), ε > 0 且满足正则性条件 a·f(n/b) ≤ c·f(n), c < 1 | T(n) = Θ(f(n)) |
要点:
- Case 2 最重要: f(n) 和 n^(log_b a) 同阶(多一个 log 因子时也无妨)
- Case 3 需要额外验证正则性条件 (regularity condition)
- 三个 Case 之间有空隙,不是所有递推式都适用 Master Theorem
经典例子:
| 递推式 | a | b | log_b a | f(n) | Case | 结果 |
|---|---|---|---|---|---|---|
| T(n) = 9T(n/3) + n | 9 | 3 | 2 | n = O(n^(2-ε)) | 1 | Θ(n²) |
| T(n) = T(2n/3) + 1 | 1 | 3/2 | 0 | 1 = Θ(n⁰) | 2 | Θ(log n) |
| T(n) = 3T(n/4) + n | 3 | 4 | log₄3 ≈ 0.793 | n = Ω(n^(0.793+ε)) | 3 | Θ(n) |
| T(n) = 2T(n/2) + n | 2 | 2 | 1 | n = Θ(n¹·log⁰ n) | 2 | Θ(n log n) |
| T(n) = T(n/2) + n | 1 | 2 | 0 | n = Ω(n^(0+ε)) | 3 | Θ(n) |
注意 : T(n) = T(n-1) + O(n) 不能用 Master Theorem(因为 b > 1 才适用)
1.3 递归树法 (Recursion Tree)
当 Master Theorem 不适用时使用:
- 画出递归树的每一层
- 计算每层的工作量
- 对所有层求和
例: T(n) = T(n/3) + T(2n/3) + O(n)
- 递归树深度:O(log n)
- 每层工作量:O(n)
- 总复杂度:O(n log n)
1.4 代入法 (Substitution Method) --- 数学归纳法证明
步骤:
- 猜测复杂度上界/下界
- 归纳假设:假设对小于 n 的情况成立
- 带入递推式证明对 n 成立
- 找到合适的常数 c, n₀
1.5 ⭐ BFS / DFS 遍历
| 算法 | 数据结构 | 应用 |
|---|---|---|
| BFS | 队列 (Queue) | 最短路径(无权图)、层次遍历 |
| DFS | 栈 (Stack) / 递归 | 连通分量、拓扑排序、环路检测 |
时间复杂度(考试常考):
| 存储方式 | BFS 时间 | BFS 空间 | DFS 时间 | DFS 空间 |
|---|---|---|---|---|
| 邻接表 (Adjacency List) | O(V + E) | O(V) | O(V + E) | O(V) |
| 邻接矩阵 (Adjacency Matrix) | O(V²) | O(V²) | O(V²) | O(V) |
⭐ 高频考点:问 BFS/DFS 在邻接表和邻接矩阵下的复杂度
- 原因: 邻接表遍历每条边一次 → O(V+E)
- 原因: 邻接矩阵检查每个顶点对所有顶点的连接 → O(V²)
2. 排序与划分算法
2.1 ⭐ QuickSort 及划分算法
Lomuto 划分
LomutoPartition(A, p, r):
pivot = A[r] // 选最后一个元素为 pivot
i = p - 1 // i 指向小于 pivot 的最后一个元素
for j = p to r-1:
if A[j] ≤ pivot:
i++
swap A[i] and A[j]
swap A[i+1] and A[r] // 把 pivot 放到正确位置
return i + 1 // 返回 pivot 的位置
特点:
- 简单但效率略低
- 单指针 i 跟踪 < pivot 的边界
Hoare 划分
HoarePartition(A, p, r):
pivot = A[p] // 选第一个元素为 pivot
i = p - 1
j = r + 1
while True:
do i++ while A[i] < pivot
do j-- while A[j] > pivot
if i < j:
swap A[i] and A[j]
else:
return j // 返回划分位置
特点:
- 双向扫描,效率更高
- 平均交换次数少于 Lomuto
- 返回的索引不一定在 pivot 的位置
⭐ Median-of-3 优化
QuickSort(A, p, r):
if p < r:
// Median-of-3: 选 A[p], A[(p+r)/2], A[r] 的中位数作为 pivot
mid = (p + r) / 2
if A[mid] < A[p]: swap A[p], A[mid]
if A[r] < A[p]: swap A[p], A[r]
if A[r] < A[mid]: swap A[mid], A[r]
swap A[mid], A[r] // 把中位数放到末尾 (配合 Lomuto)
q = Partition(A, p, r)
QuickSort(A, p, q-1)
QuickSort(A, q+1, r)
效果 : 避免在已排序数组上出现 O(n²) 的最坏情况
QuickSort 复杂度
| 情况 | 复杂度 | 递推式 |
|---|---|---|
| 最好 | O(n log n) | T(n) = 2T(n/2) + O(n) → 均匀划分 |
| 平均 | O(n log n) | |
| 最坏 | O(n²) | T(n) = T(n-1) + O(n) → 极度不平衡划分 |
⭐ 高频考点:何时最坏?已排序数组 + 每次都选第一个/最后一个元素作为 pivot
3. ⭐ 动态规划 (DP)
核心思想
- 最优子结构:问题的最优解包含子问题的最优解
- 重叠子问题:子问题被重复求解(与分治法的区别)
- 填表顺序:由小到大
- 核心三要素:状态定义 → 状态转移方程 → 初始化 + 边界条件
3.1 ⭐⭐ 0-1 背包问题 (高频压轴题)
问题: n 个物品,每个 i 有重量 w_i 和价值 v_i,背包容量 W,每个物品最多拿一个
状态定义: dpiw = 前 i 个物品在容量为 w 时能取得的最大价值
状态转移方程:
dp[i][w] = max(dp[i-1][w], // 不拿第 i 个物品
dp[i-1][w - w_i] + v_i) // 拿第 i 个物品 (前提 w ≥ w_i)
初始化: dp0w = 0, dpi0 = 0
时间复杂度 : O(n·W) --- 伪多项式时间(n 为物品数,W 为容量)
填表示例:
物品: (w,v) = (2,3), (3,4), (4,5), (5,6); W = 8
w=0 w=1 w=2 w=3 w=4 w=5 w=6 w=7 w=8
i=0 0 0 0 0 0 0 0 0 0
i=1(2,3) 0 0 3 3 3 3 3 3 3
i=2(3,4) 0 0 3 4 4 7 7 7 7
i=3(4,5) 0 0 3 4 5 7 8 9 9
i=4(5,6) 0 0 3 4 5 7 8 9 10
回溯找到选取的物品: 从 dpnW 开始,如果 dpiw ≠ dpi-1w,说明拿了 i
3.2 硬币找零(无限数量)
问题: 给定硬币面额 coins = 1,3,5,无限使用,凑成金额 amount,求最少硬币数
状态定义: dpi = 凑成金额 i 所需的最少硬币数
状态转移方程:
dp[i] = min(dp[i], dp[i - coin] + 1) // 对每种 coin ≤ i
初始化: dp0 = 0, dp其他 = ∞ (或一个大数)
时间复杂度 : O(n·m) (n 为金额,m 为硬币种类数)
填表示例: coins = 1,3,5, amount = 11
dp[0]=0 dp[1]=1 dp[2]=2 dp[3]=1 dp[4]=2 dp[5]=1
dp[6]=2 dp[7]=3 dp[8]=2 dp[9]=3 dp[10]=2 dp[11]=3
答案: dp11 = 3 (5+5+1 或 5+3+3)
3.3 ⭐ LCS(最长公共子序列)
问题: 求两个序列 X1...m, Y1...n 的最长公共子序列长度
状态定义: dpij = X1...i 和 Y1...j 的 LCS 长度
状态转移方程:
dp[i][j] = dp[i-1][j-1] + 1, 如果 X[i] = Y[j]
dp[i][j] = max(dp[i-1][j], dp[i][j-1]), 如果 X[i] ≠ Y[j]
初始化: dp0j = 0, dpi0 = 0
时间复杂度 : O(m·n)
空间复杂度: O(m·n)(可优化为 O(min(m,n)))
回溯:
while i > 0 and j > 0:
if X[i] == Y[j]:
打印 X[i]; i--; j--
else if dp[i-1][j] > dp[i][j-1]:
i--
else:
j--
示例: X = "ABCBDAB", Y = "BDCAB"
- LCS 长度 = 4
- LCS = "BCAB" 或 "BDAB"
3.4 ⭐ OBST(最优二叉搜索树)
问题: 给定键值 K₁ < K₂ < ... < Kₙ 及其搜索概率 p₁, p₂, ..., pₙ,构造期望搜索代价最小的 BST
核心公式:
递推式1 (含 dummy 键概率 q_i):
- eij = min{eir-1 + er+1j + wij}, i ≤ r ≤ j
- wij = wir-1 + p_r + wr+1j = wij-1 + p_j + q_j
递推式2(考试常用简化版,不加 dummy 键):
- costij = min{costir-1 + costr+1j + sum(pi...j)}, i ≤ r ≤ j
- 其中 sum(pi...j) = p_i + p_{i+1} + ... + p_j
时间复杂度 : O(n³) --- 三层循环(区间长度 × 起点 × 根位置)
填表顺序: 按区间长度从小到大,从 len=1 到 len=n
填写规则:
| dpij | 含义 |
|---|---|
| i = j | costii = p_i (单节点树, 即该节点的概率) |
| i > j | costij = 0 (空树) |
| i < j | 在 r ∈ i, j 中找最小 |
示例: n=3, p=0.1, 0.2, 0.4
cost[1][1]=0.1 cost[2][2]=0.2 cost[3][3]=0.4
cost[1][2]=min(
r=1: cost[1][0]+cost[2][2]+(0.1+0.2) = 0+0.2+0.3 = 0.5
r=2: cost[1][1]+cost[3][2]+(0.1+0.2) = 0.1+0+0.3 = 0.4
) = 0.4 (根为 K₂)
...
DP 对比总结
| 问题 | 状态维度 | 时间复杂度 | 空间复杂度 | 核心公式 |
|---|---|---|---|---|
| 0-1 背包 | 2D (n×W) | O(n·W) | O(n·W) | max(不拿, 拿) |
| 硬币找零 | 1D | O(n·m) | O(n) | min(不加, 加1) |
| LCS | 2D (m×n) | O(m·n) | O(m·n) | 匹配+1 / max |
| OBST | 2D (n×n) | O(n³) | O(n²) | min(左+右+sum) |
DP 考试技巧:
- 看到"最优化"、"最大/最小"、"方案数" → 考虑 DP
- 状态定义是最关键的一步
- 转移方程确定后,关注填表顺序(决定能否用滚动数组优化)
- 如果要求输出具体方案 → 记住回溯的步骤
4. ⭐ 最短路径算法
4.1 ⭐⭐ Dijkstra 算法
适用 : 单源最短路径,非负权重图
核心思想: 贪心,每次从未确定最短距离的顶点中选最近的加入 S 集
Dijkstra(G, s):
初始化 dist[s] = 0, dist[其他] = ∞
S = ∅ // 已确定最短路径的顶点集合
Q = V // 优先队列(最小堆),按 dist 排序
while Q ≠ ∅:
u = Extract-Min(Q) // 从 Q 中取出 dist 最小的顶点
S = S ∪ {u}
for each v in Adj[u]:
Relax(u, v, w) // 松弛操作
if dist[u] + w(u,v) < dist[v]:
dist[v] = dist[u] + w(u,v)
Decrease-Key(Q, v, dist[v]) // 更新 Q 中的优先级
Relax 松弛操作:
if dist[u] + w(u,v) < dist[v]:
dist[v] = dist[u] + w(u,v)
时间复杂度 : O((V + E) log V)
原因: 每个顶点 Extract-Min 一次 (V × O(log V)),每条边 Relax/Decrease-Key 一次 (E × O(log V))
正确性条件 : 适用于非负权重(负权重会使贪心失效)
步骤示例:

| 步骤 | S 集合 | dist 数组 |
|---|---|---|
| 初始 | {a} | a:0, b:∞, c:∞, d:∞ |
| 1 | {a,b} | a:0, b:5, c:∞, d:∞ |
| 2 | {a,b,c} | a:0, b:5, c:9, d:∞ |
| 3 | {a,b,c,d} | a:0, b:5, c:9, d:11 |
⭐ 关键理解: 为什么 Dijkstra 不能处理负权重?因为已加入 S 的顶点不会再被更新,而负边可能使已确定的最短路径变短。
4.2 ⭐ Bellman-Ford 算法
适用 : 单源最短路径,允许负权重,可检测负环
Bellman-Ford(G, s):
初始化 dist[s] = 0, dist[其他] = ∞
// V-1 轮松弛
for i = 1 to |V| - 1:
for each edge (u, v) in E:
Relax(u, v, w)
// 检测负环
for each edge (u, v) in E:
if dist[u] + w(u,v) < dist[v]:
return FALSE // 存在负环
return TRUE
时间复杂度 : O(V·E)
原因: V-1 轮外层循环 × E 条边的内层循环
为什么是 V-1 轮?
- 最长简单路径最多 V-1 条边
- 经过 V-1 轮松弛,所有顶点的最短距离必然确定
检测负环: 第 V 轮仍然能松弛 → 存在负环
⭐ 高频考点: Bellman-Ford vs Dijkstra 的区别
4.3 DAG 最短路径
适用 : 有向无环图 (DAG),权重任意
DAG-Shortest-Path(G, s):
对 G 进行拓扑排序
初始化 dist[s] = 0, dist[其他] = ∞
for each vertex u in topological order:
for each edge (u, v) in Adj[u]:
Relax(u, v, w)
时间复杂度 : O(V + E)
原因: 拓扑排序 O(V+E) + 每条边松弛一次 O(E)
4.4 Floyd-Warshall 算法
适用 : 所有点对最短路径,DP 思想
Floyd-Warshall(G):
初始化 dist[i][j] = w(i,j) // 直接边权重
dist[i][i] = 0
若无直接边: dist[i][j] = ∞
for k = 1 to V:
for i = 1 to V:
for j = 1 to V:
dist[i][j] = min(dist[i][j],
dist[i][k] + dist[k][j])
时间复杂度 : O(V³)
原因: 三层嵌套循环
核心思想: DP 递推 --- 经过前 k 个顶点中转的最短路径
空间复杂度: O(V²) --- 可直接在原数组上更新
4.5 ⭐ Johnson 算法
适用 : 所有点对 最短路径,有负边但无负环,稀疏图友好
核心思想: 重新赋权 (Reweighting) 将负权边转为非负,然后对每个顶点跑 Dijkstra
Johnson(G):
// 步骤1: 添加超级源点 s*,到所有顶点连 0 权边
G' = G 加上新顶点 s*
for each v in V:
add edge (s*, v) with w = 0
// 步骤2: 跑 Bellman-Ford 得到 h(v) = dist(s*, v)
if Bellman-Ford(G', s*) 检测到负环:
return "存在负环"
// 步骤3: 重新赋权
for each edge (u, v) in E:
w'(u,v) = w(u,v) + h(u) - h(v)
// 步骤4: 对每个顶点跑 Dijkstra
for each vertex u in V:
Dijkstra(G, u) // 用 w' 权重
// 恢复原始距离: dist(u,v) = dist'(u,v) - h(u) + h(v)
时间复杂度 : O(V² log V + V·E)
原因分解:
- Bellman-Ford: O(V·E)
- 重新赋权: O(E)
- V 次 Dijkstra: V × O((V+E) log V) = O(V² log V + V·E)
重新赋权为什么能保证非负?
- 由三角不等式 h(v) ≤ h(u) + w(u,v)
- 所以 w'(u,v) = w(u,v) + h(u) - h(v) ≥ 0 ✓
最短路径算法对比
| 算法 | 适用场景 | 时间复杂度 | 空间复杂度 | 负边 | 负环检测 |
|---|---|---|---|---|---|
| Dijkstra | 非负权单源 | O((V+E)log V) | O(V) | ❌ | ❌ |
| Bellman-Ford | 任意单源 | O(V·E) | O(V) | ✅ | ✅ |
| DAG 最短路径 | DAG 单源 | O(V+E) | O(V) | ✅ | ✅(无环) |
| Floyd-Warshall | 任意全源 | O(V³) | O(V²) | ✅ | 可检测 |
| Johnson | 负边全源 | O(V²log V + VE) | O(V²) | ✅ | ✅ |
算法选择指南:
- 无负边 + 单源 → Dijkstra
- 有负边 + 单源 → Bellman-Ford
- 有负边 + 全源 + 稀疏图 → Johnson
- 密集图 + 全源 → Floyd-Warshall
5. ⭐ 最小生成树 (MST)
5.1 ⭐ Kruskal 算法
核心思想 : 贪心,从小到大选边,不能形成环
Kruskal(G):
A = ∅ // MST 边集合
for each v in V:
Make-Set(v) // 每个顶点初始化为一个集合
将所有边按权重从小到大排序
for each edge (u, v) in sorted edges:
if Find-Set(u) ≠ Find-Set(v): // u 和 v 不在同一连通分量
A = A ∪ {(u, v)}
Union(u, v) // 合并两个集合
return A
时间复杂度 : O(E log E)
原因: 排序 O(E log E) + 并查集操作 O(E·α(V)) ≈ O(E)
关键数据结构 : 并查集 (Union-Find) --- Find 和 Union 近似 O(1)
5.2 ⭐ Prim 算法
核心思想: 从一个顶点出发,贪心地选择连接树内和树外顶点的最小边
Prim(G, start):
for each v in V:
key[v] = ∞ // 到 MST 的最小距离
parent[v] = NIL
key[start] = 0
Q = V // 优先队列(按 key 排序)
while Q ≠ ∅:
u = Extract-Min(Q)
for each v in Adj[u]:
if v in Q and w(u,v) < key[v]:
parent[v] = u
key[v] = w(u,v)
Decrease-Key(Q, v, key[v])
时间复杂度 : O(E log V)
原因: 每个顶点 Extract-Min 一次 (V × O(log V)),每条边可能 Decrease-Key (E × O(log V))
使用斐波那契堆可优化到 O(E + V log V)
Kruskal vs Prim 对比
| 特性 | Kruskal | Prim |
|---|---|---|
| 策略 | 选全局最小边 | 选连接树的最小边 |
| 适合 | 稀疏图 (E ≈ V) | 稠密图 (E ≈ V²) |
| 时间复杂度 | O(E log E) | O(E log V) |
| 数据结构 | 并查集 | 优先队列/最小堆 |
| 实现难度 | 较简单 | 略复杂 |
| 边排序 | 需要 | 不需要 |
⭐ 注意: 两种算法等价:Kruskal 全局排序选边,Prim 局部贪心扩展
MST 重要性质:
- Cut Property: 对任意割,最小权重的跨越边一定在 MST 中
- Cycle Property: 对任意环,最大权重的边一定不在 MST 中
6. ⭐ 最大流 (Maximum Flow)
6.1 ⭐⭐ Edmonds-Karp (EK) 算法
核心思想 : 用 BFS 在残量网络中找最短增广路径(边数最少)
Edmonds-Karp(G, s, t):
for each edge (u,v) in E:
flow[u][v] = 0 // 初始化流量为 0
while BFS 在残量网络中能找到从 s 到 t 的路径:
// BFS 找到一条最短增广路径 P
// 计算瓶颈容量: bottleneck = min P 上的残量容量
bottleneck = ∞
for each edge (u,v) in P:
bottleneck = min(bottleneck,
c[u][v] - flow[u][v]) // 正向边
// 或 flow[v][u] // 反向边
// 更新路径上的流量
for each edge (u,v) in P:
flow[u][v] += bottleneck // 正向边加流量
flow[v][u] -= bottleneck // 反向边减流量
return total_flow // 从 s 出发的总流量
时间复杂度 : O(V·E²)
原因: 每次 BFS O(E),最多 O(V·E) 次增广(每次增广至少增加 1 条饱和边,每条边最多饱和 V/2 次)
BFS 找最短增广路径: 在残量网络中从 s 到 t 找边数最少的路径
伪代码: 可用 parent 数组记录路径,类似 BFS 找无权图最短路径
6.2 核心概念
| 概念 | 定义 |
|---|---|
| 残量网络 (Residual Network) | G_f = (V, E_f),包含正向残量边 (c - f > 0) 和反向残量边 (f > 0) |
| 增广路径 (Augmenting Path) | 残量网络中 s → t 的路径 |
| 瓶颈容量 (Bottleneck) | 增广路径上最小的残量容量 |
| 割 (Cut) | (S, T) 将 V 分为两部分,s ∈ S, t ∈ T |
| 割容量 | Σ c(u,v), u ∈ S, v ∈ T, (u,v) ∈ E |
6.3 ⭐⭐⭐ 最大流最小割定理
定理 : 在任意网络中,最大流的值 等于 最小割的容量
max _ f l o w = min _ c u t c a p a c i t y ( S , T ) \max\flow = \min\{cut} capacity(S, T) max_flow=min_cutcapacity(S,T)
推论:
- 流值 ≤ 任意割的容量
- 若流值等于某个割的容量 → 该流为最大流,该割为最小割
⭐ 重难点: 理解'反向边'的作用 --- 反向边提供"撤销"功能,允许算法修正之前的不良选择
考试常考: 给定网络找最大流、最小割、割容量
6.4 EK 算法执行示例
网络: s → a (cap=10), s → b (cap=5)
a → b (cap=15), a → t (cap=5), b → t (cap=10)
步骤1: BFS 找最短增广路径: s → a → t, bottleneck = 5
步骤2: BFS 找最短增广路径: s → b → t, bottleneck = 5
步骤3: BFS 找最短增广路径: s → a → b → t, bottleneck = 5
最大流 = 15
最小割: S = {s, a, b}, T = {t}, 割容量 = 5+10 = 15 ✓
7. ⭐ 二分图匹配 (Bipartite Matching)
7.1 基本概念
- 二分图: 顶点可划分为两个不相交集合 U 和 V,所有边连接 U 和 V 中的顶点
- 匹配 (Matching): 边的一个子集,其中任意两条边没有公共顶点
- 最大匹配: 边数最多的匹配
- 完美匹配: 覆盖所有顶点的匹配
- 交替路径: 已匹配边和未匹配边交替出现的路径
- 可增广路径 (Augmenting Path): 起点和终点都是未匹配顶点的交替路径
7.2 ⭐⭐ Berge's Lemma
定理 : 匹配 M 是最大匹配 当且仅当 不存在关于 M 的增广路径
7.3 ⭐⭐ 匈牙利树算法 (Hungarian Tree Algorithm)
核心思想: 在二分图中用 DFS/BFS 寻找可增广路径,找到则翻转路径上的边
Hungarian(G, U, V, E):
初始化匹配 M = ∅
for each vertex u in U:
if u 未匹配:
visited = ∅ // 每轮重置已访问标记
if DFS(u) 找到增广路径:
翻转路径上的边状态
matching_count++
DFS(u):
for each v in Adj[u]: // u 的邻居
if v not visited:
visited[v] = true
if v 未匹配 或 DFS(match[v]) 成功: // 找增广路径
match[v] = u
return TRUE
return FALSE
时间复杂度 : O(V·E)
原因: 最多 V 次 DFS/BFS,每次 O(E)
DFS/BFS 搜索增广路径:
- 从未匹配的左顶点开始
- 走交替路径: 未匹配边 → 匹配边 → 未匹配边 → ...
- 直到找到未匹配的右顶点
- 翻转路径上的所有边: 匹配 ↔ 未匹配
匈牙利树算法执行示例
U = {1, 2, 3}, V = {a, b, c, d}
边: 1-a, 1-b, 2-a, 2-c, 3-b, 3-d
步骤1: 从 1 出发, DFS: 1→a (a未匹配) → 匹配 1-a
匹配: M = {(1,a)}
步骤2: 从 2 出发, DFS: 2→a (a已匹配1) → 1→b (b未匹配) → 翻转
匹配: M = {(2,a), (1,b)}
步骤3: 从 3 出发, DFS: 3→b (b已匹配1) → 1→a (a已匹配2) → 2→c (c未匹配) → 翻转
匹配: M = {(3,b), (1,a), (2,c)}
最大匹配数 = 3
7.4 二分图匹配的 ES 问题 (Exact Satisfiability)
- 输入: 二分图 G = (U ∪ V, E) 和集合族 F
- 问题: 是否存在匹配 M 使得每个集合被精确覆盖一次
- 解法: 构造流网络,每条边容量 1,用最大流求解
二分图匹配与最大流的关系
二分图最大匹配可以转化为最大流问题:
- 添加超级源点 s 连左部所有顶点 (cap=1)
- 原二分图边改为从左到右的有向边 (cap=1)
- 右部所有顶点连超级汇点 t (cap=1)
- 最大匹配数 = 最大流值
8. ⭐ 线性规划 (Linear Programming)
8.1 标准形式
最小化形式 → 标准形式转换规则:
| 原始形式 | 转换方法 |
|---|---|
| min → max | 目标函数取负 |
| 约束 ≥ | 两边取负转为 ≤ |
| 约束 = | 拆为 ≤ 和 ≥ 两个约束 |
| 自由变量 x | x = x⁺ - x⁻, x⁺ ≥ 0, x⁻ ≥ 0 |
| 变量 x ≤ 有限值 | x' = M - x 代入 |
| 变量 x ≥ 有限值 | x' = x - L 代入 |
标准形式 (变量 ≥ 0):
maximize: c₁x₁ + c₂x₂ + ... + cₙxₙ
subject to: a₁₁x₁ + a₁₂x₂ + ... + a₁ₙxₙ ≤ b₁
a₂₁x₁ + a₂₂x₂ + ... + a₂ₙxₙ ≤ b₂
...
aₘ₁x₁ + aₘ₂x₂ + ... + aₘₙxₙ ≤ bₘ
x₁, x₂, ..., xₙ ≥ 0
8.2 松弛形式 (Slack Form)
-
将不等式约束转化为等式约束(引入松弛变量 s₁, s₂, ..., sₘ)
-
松弛变量 ≥ 0
原始: a₁₁x₁ + a₁₂x₂ + ... + a₁ₙxₙ ≤ b₁
转换: a₁₁x₁ + a₁₂x₂ + ... + a₁ₙxₙ + s₁ = b₁, s₁ ≥ 0
8.3 基本解 (Basic Solution)
- 基本变量 (Basic variables, B): m 个变量,构成基
- 非基本变量 (Nonbasic variables, N): n 个变量,设为 0
- 基本解: 非基本变量 = 0,解方程组得基本变量的值
- 可行基本解 (Basic Feasible Solution, BFS): 所有变量 ≥ 0 的基本解
8.4 ⭐⭐⭐ Simplex 算法(单纯形法)
核心思想: 沿可行域的边移动,每次迭代增加目标函数值
Simplex(A, b, c):
将 LP 转换为松弛形式
找到初始 BFS (所有非基本变量 = 0)
while True:
// STF (Select Target Function): 选择目标系数为正的 NB 变量
// 选正系数最大的那个(加速收敛)
if 目标函数中所有 NB 变量的系数 ≤ 0:
break // 达到最优解
选某个 NB 变量 x_e (系数 > 0) 进入基
// RLF (Ratio Limit Function): 找最紧约束
min_ratio = ∞
for each 基本变量对应约束 i:
if a_{i,e} > 0:
ratio = b_i / a_{i,e}
if ratio < min_ratio:
min_ratio = ratio
pivot_row = i
if min_ratio == ∞:
return "无界" // 目标函数可以无限增大
选对应的基本变量 x_l 离开基
// Pivot: 交换 x_e 和 x_l
// 1. 将 x_e 用 x_l 和其他变量表示
// 2. 代入其他所有方程
// 3. 更新目标函数
选择进基变量 (STF - Select Target Function):
- 在目标函数中找到系数为正的非基本变量
- 如果有多个正系数,通常选最大的(但选择策略影响收敛速度)
选择离基变量 (RLF - Ratio Limit Function):
- 对每个约束,计算 b_i / a_{i,e}(其中 a_{i,e} > 0)
- 选最小比值对应的基本变量离基
- 这是保证新解可行的关键
Pivot 操作:
// 假设 xe 进基, xl 离基
// 将等式 l 写成 xe 的形式:
xe = bl/a_{l,e} - Σ a_{l,j}/a_{l,e} · xj - xl/a_{l,e}
// 代入其他所有等式和目标函数
终止条件 : 目标函数中所有非基本变量系数均为负值(最大值问题)
单纯形法示例
maximize: z = 3x₁ + 2x₂
约束: x₁ + x₂ ≤ 4
2x₁ + x₂ ≤ 6
x₁, x₂ ≥ 0
步骤1: 转为松弛形式
z = 3x₁ + 2x₂
x₁ + x₂ + s₁ = 4
2x₁ + x₂ + s₂ = 6
x₁, x₂, s₁, s₂ ≥ 0
N = {x₁, x₂}, B = {s₁, s₂}
步骤2: 选 x₁ 进基(系数 3 > 0), 算比值: 4/1=4, 6/2=3 → s₂ 离基
步骤3: Pivot → 新基 B = {s₁, x₁}, N = {x₂, s₂}
x₁ = 3 - 0.5x₂ - 0.5s₂
s₁ = 1 - 0.5x₂ + 0.5s₂
z = 9 + 0.5x₂ - 1.5s₂
步骤4: 选 x₂ 进基(系数 0.5 > 0), 算比值: 1/0.5=2, 3/0.5=6 → s₁ 离基
步骤5: Pivot → 新基 B = {x₂, x₁}, N = {s₁, s₂}
x₂ = 2 - 2s₁ + s₂
x₁ = 2 + s₁ - s₂
z = 10 - 3s₁ - 0.5s₂
所有系数 ≤ 0 → 最优解: x₁=2, x₂=2, z=10 ✓
单纯形法注意点
- 退化 (Degeneracy): 某个基本变量为 0,可能导致循环(多个 pivot 后回到同一 BFS)
- Bland's 规则: 选最小下标变量可避免循环
- 初始 BFS 不可行: 需要两阶段法(Phase I: 构造辅助 LP 求可行解)
9. 考试热点速查表
⭐⭐ 极端重点 (必考)
| 知识点 | 考查形式 | 关键记忆点 |
|---|---|---|
| DP (0-1背包/LCS) | 算法设计题(20分) | 状态定义 + 转移方程 + 填表 + 回溯 |
| Edmonds-Karp 最大流 | 计算题 | BFS找增广路 + 残量网络 + 最大流最小割 |
| Johnson 算法 | 选择/判断/计算 | 超级源 → Bellman-Ford → 重新赋权 → Dijkstra × V |
| 匈牙利树/二分图匹配 | 算法设计/计算 | 增广路径 + Berge's Lemma + DFS |
| 线性规划 Simplex | 计算题 | 标准形式 + STF + RLF + Pivot |
| Dijkstra vs Bellman-Ford | 选择/判断 | 适用条件 + 复杂度 + 正确性证明 |
| Kruskal / Prim | 计算题 | 贪心 + 选边/选顶点 |
⭐ 次重点
| 知识点 | 考查形式 |
|---|---|
| Master Theorem (3个 Case) | 选择题 |
| BFS/DFS 复杂度 (邻接表 vs 邻接矩阵) | 选择题 |
| QuickSort 划分 (Lomuto/Hoare/Median-of-3) | 选择题 |
| Floyd-Warshall | 选择/判断 |
| Bellman-Ford 检测负环 | 选择/判断 |
| OBST 递推公式 | 选择/判断 |
| 最大流最小割定理 | 选择/填空 |
❌ 不考内容
| 内容 | 说明 |
|---|---|
| 矩阵运算 | 不做考查 |
| Ford-Fulkerson 方法 (非 EK) | 只考 EK |
| DP AllPathShortPath | 不要求 |
| 时间复杂度形式化证明 | 只考 Master Theorem (选择题) |
| 贪心算法证明 | 需会分析但不考证明 |
算法设计题答题模板 (20分 × 2)
- 问题分析: 指出属于哪类问题(最短路径/最大流/DP/二分图匹配)
- 数据结构定义: 说明用什么数据结构存储
- 算法步骤 :
- 初始化
- 核心循环/递推
- 终止条件
- 示例运行: 用简单例子展示算法执行过程
- 复杂度分析: 给出时间复杂度和简要解释
附:常用时间复杂度速查
O(1) --- 常数时间
O(log n) --- 对数时间 (二分查找)
O(n) --- 线性时间 (遍历)
O(n log n) --- 线性对数 (排序)
O(n²) --- 平方时间 (双层循环)
O(n³) --- 立方时间 (Floyd-Warshall)
O(2ⁿ) --- 指数时间 (暴力搜索)
O(n!) --- 阶乘时间 (全排列)
| 算法 | 时间复杂度 |
|---|---|
| BFS/DFS (邻接表) | O(V + E) |
| BFS/DFS (邻接矩阵) | O(V²) |
| QuickSort (平均) | O(n log n) |
| QuickSort (最坏) | O(n²) |
| Dijkstra | O((V+E) log V) |
| Bellman-Ford | O(V·E) |
| Floyd-Warshall | O(V³) |
| Johnson | O(V² log V + V·E) |
| Kruskal | O(E log E) |
| Prim | O(E log V) |
| Edmonds-Karp | O(V·E²) |
| Hungarian Tree | O(V·E) |
| LCS | O(m·n) |
| 0-1 Knapsack | O(n·W) |
| Coin Change | O(n·amount) |
| OBST | O(n³) |
| Simplex | 指数级最坏 (但实际高效) |
| 拓扑排序 | O(V + E) |
最后叮嘱 : 考试重点是图论问题 (第 3 章及之后),DP + Johnson + 匈牙利树 + 线性规划 + EK 最大流 + MST 是热门考点。算法设计题一定要写出伪代码 + 示例 + 复杂度!
祝考试顺利!🎯