算法设计 期末复习笔记

算法设计 期末复习笔记

基于 Lecture 1-7, 作业与考试热点整理

考试题型: 10 选择(20分) + 10 判断(10分) + 4 计算 + 2 算法设计


占位图

目录

  1. 算法基础与复杂度分析
  2. 排序与划分算法
  3. 动态规划 (DP)
  4. 最短路径算法
  5. 最小生成树 (MST)
  6. 最大流 (Maximum Flow)
  7. 二分图匹配 (Bipartite Matching)
  8. 线性规划 (Linear Programming)
  9. 附:考试热点速查表

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) --- 数学归纳法证明

步骤:

  1. 猜测复杂度上界/下界
  2. 归纳假设:假设对小于 n 的情况成立
  3. 带入递推式证明对 n 成立
  4. 找到合适的常数 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)

核心思想

  1. 最优子结构:问题的最优解包含子问题的最优解
  2. 重叠子问题:子问题被重复求解(与分治法的区别)
  3. 填表顺序:由小到大
  4. 核心三要素:状态定义 → 状态转移方程 → 初始化 + 边界条件

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 考试技巧:

  1. 看到"最优化"、"最大/最小"、"方案数" → 考虑 DP
  2. 状态定义是最关键的一步
  3. 转移方程确定后,关注填表顺序(决定能否用滚动数组优化)
  4. 如果要求输出具体方案 → 记住回溯的步骤

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)

  1. 问题分析: 指出属于哪类问题(最短路径/最大流/DP/二分图匹配)
  2. 数据结构定义: 说明用什么数据结构存储
  3. 算法步骤 :
    • 初始化
    • 核心循环/递推
    • 终止条件
  4. 示例运行: 用简单例子展示算法执行过程
  5. 复杂度分析: 给出时间复杂度和简要解释

附:常用时间复杂度速查

复制代码
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 是热门考点。算法设计题一定要写出伪代码 + 示例 + 复杂度

祝考试顺利!🎯

相关推荐
鱼儿也有烦恼2 小时前
题型归纳与重要模版
算法
菜鸟不爱学2 小时前
第P4周:猴痘病识别
算法
QN1幻化引擎2 小时前
我写了一个「有八层意识结构」的 Python 认知引擎,它没有用任何 LLM
人工智能·算法·架构
Keven_114 小时前
算法札记:如何卡SPFA
算法·spfa
imuliuliang4 小时前
关于并发哈希表的锁分段机制与冲突减少策略7
算法
可编程芯片开发4 小时前
基于电压电流双闭环控制的三相整流器系统simulink建模与仿真
算法
可编程芯片开发4 小时前
基于ADRC自抗扰算法的UAV飞行姿态控制系统simulink建模与仿真
算法
学究天人4 小时前
数学公理体系大全:第七章 连续统假设与力迫法简介
人工智能·算法·机器学习·数学建模·动态规划·图论·抽象代数
Keven_114 小时前
算法札记:SPFA什么时候用队列什么时候用栈
算法·spfa