跳转至

十一、网络流与最小生成树

考点速览

# 考点 题型 重要度
1 最大流定义与流量守恒 填空 / 选择 ★★★
2 残余图 \(G_r\) 的构造(含反向边) 画图 / 大题 ★★★★★
3 增广路径迭代求最大流 大题(手算) ★★★★★
4 贪心选第一条路径为何错 简答 ★★★
5 增广算法的三种复杂度 填空 / 选择 ★★★★
6 Prim 算法手算 大题(画 MST) ★★★★★
7 Kruskal 算法手算 + 并查集判环 大题 ★★★★★
8 Prim vs Kruskal 复杂度选择 选择 ★★★
9 生成树的基本性质(边数 / 加边成环) 填空 ★★★

§4 网络流问题(Network Flow Problems)

考点 1:问题定义与守恒约束

给定有向图 \(G\)

  • 源点 \(s\):只有流出
  • 汇点 \(t\):只有流入
  • 每条边 \((v, w)\)容量 \(c_{v,w}\)

流量守恒:对所有 \(v \notin \{s, t\}\)

\[ \sum_{u} f_{u,v} \;=\; \sum_{w} f_{v,w} \]

目标:求 \(s \to t\) 能通过的最大流量

易错点

  • 容量是的属性,不是顶点的属性。
  • 守恒不包括 \(s\)\(t\)\(s\) 流出量 = \(t\) 流入量 = 当前流值 \(f\)

考点 2 + 3:Ford-Fulkerson 算法(FF 算法)

这就是 PPT 中讲的那套"残余图 + 增广路径"方法,学术名称为 Ford-Fulkerson Method

三个图——一图看懂

原始图 G                流图 Gf               残余图 Gr
(标的是容量)           (标的是已用流)         (标的是还能走多少)

  s ──3──▶ a           s ──2──▶ a           s ──1──▶ a        ← 正向剩余
  │         │           │         │           │         │
  2         2           0         2           2         0
  ▼         ▼           ▼         ▼           ▼         ▼
  b ──3──▶ t           b ──0──▶ t           b ──3──▶ t
                                              a ──2──▶ s      ← 反向边!
                                              t ──2──▶ a      ← 反向边!
符号 含义 边上标什么
\(G\) 原始图 容量 \(c_{v,w}\)
\(G_f\) 流图 当前已分配的流 \(f_{v,w}\)
\(G_r\) 残余图 正向 \(c-f\),反向 \(f\)(可撤销量)

残余图构造规则(考试必考 ★★★★★)

对原图中每一条有向边 \((v, w)\)

        原边容量 c = 5,当前流 f = 3

正向:v ──(c-f=2)──▶ w    "还能再多送 2 个单位"
反向:w ──(f=3)────▶ v    "之前送了 3,最多可以撤回 3"

三条铁律:

  1. 正向残余 = \(c_{v,w} - f_{v,w}\)(还能再走多少)
  2. 反向残余 = \(f_{v,w}\)(已用流 = 可撤回量)
  3. 残余为 0 的边删掉

FF 算法步骤(背下来)

Ford-Fulkerson(G, s, t):
    初始化所有边 f = 0
    while (Gr 中存在 s→t 的路径 P):  ← 增广路径
        Δ = min(P 上所有边的残余容量)  ← 瓶颈
        for P 上每条边 (u,v):
            if (u,v) 是正向边: f(u,v) += Δ
            if (u,v) 是反向边: f(v,u) -= Δ  ← 撤销
        更新 Gr
    return 当前总流值

完整例题走查:一步步跑 FF 算法

原图 \(G\)(5 条边,4 个节点):

        s
       / \
    (1)   (1)
     ↓     ↓
     a ──▶ b        a→b 容量 1
     |     |
    (1)   (1)
     ↓     ↓
        t
容量
s→a 1
s→b 1
a→b 1
a→t 1
b→t 1

最大流应该 = 2(很直观:\(s \to a \to t\) 走 1,\(s \to b \to t\) 走 1)。


❌ 如果不加反向边(纯贪心)

第 1 轮:贪心选了 \(s \to a \to b \to t\)\(\Delta = 1\)

流图 Gf:           残余图 Gr(无反向边版本):
     s                   s
     |                    \
  f=1↓                   (1)   ← s→b 还有
     a ─f=1─▶ b           ↓
              |           b
           f=1↓           (a→t 还有但到不了 a)
              t           (死路!s→a 满了)

第 2 轮:找 \(s \to t\) 的路径...

  • \(s \to b \to ?\)\(b \to t\) 已满!
  • \(s \to a\):已满!

卡住了!总流 = 1 ≠ 正确答案 2。


✅ 加反向边(正确的 FF 算法)

第 1 轮:同样选 \(s \to a \to b \to t\)\(\Delta = 1\)

残余图 Gr(有反向边版本):
       s
        \
        (1)          s→b 剩余
    ┌──▶ b ←─(1)── a   ← 反向边 b→a 出现了!
    |         
(1) |    a→s: (1)    ← 反向边
    |    t→b: (1)    ← 反向边
    |    a→t: (1)    ← a→t 从未用过
    无(b→t 已满)

整理残余图 Gr 的边:
  s→b: 1(未用过)
  a→t: 1(未用过)
  a→s: 1(反向,s→a 的撤销边)
  b→a: 1(反向,a→b 的撤销边)
  t→b: 1(反向,b→t 的撤销边)

第 2 轮:在 \(G_r\) 中找 \(s \to t\)

\[ s \xrightarrow{1} b \xrightarrow{1} a \xrightarrow{1} t \]

注意:\(b \to a\)反向边!走它意味着撤销之前 \(a \to b\) 的那 1 单位流。

\(\Delta = \min(1, 1, 1) = 1\)

更新后的流图 \(G_f\)

        s                     解读:
       / \                    • s→a: 流 1(未变)
    f=1   f=1                 • s→b: 流 1(新增)
     ↓     ↓                  • a→b: 流 0(被撤销了!)
     a     b                  • a→t: 流 1(新增)
     |     |                  • b→t: 流 1(未变)
   f=1   f=1
     ↓     ↓
        t

总流 = 2 ✓(\(s\) 流出 \(1+1=2\)\(t\) 流入 \(1+1=2\)

核心直觉

走反向边 \(b \to a\) = 把之前走错的 \(a \to b\) 退回来,让 \(a\) 的流量改道去 \(t\),让 \(b\) 的流量由 \(s\) 直接供给。


考点 4:为什么必须加反向边

无反向边(纯贪心) 有反向边(FF 算法)
第一次选错了怎么办 死了,无法纠正 走反向边 = 撤销
上面例子结果 流 = 1(错) 流 = 2(对)
本质 一条路走到黑 允许「后悔」

定理(必背)

若所有边容量为有理数,Ford-Fulkerson 算法必终止且得到最大流
含环图同样成立。

易错点

  • 反向边的容量 = 当前已用流量,不是 0,也不是原容量。
  • 同一条原边可以同时在 \(G_r\) 中有正向 + 反向两个身份(部分用了部分没用时)。
  • 走反向边后,对应的正向流量要减少(撤销),不是增加。

考点 5:复杂度分析(填空 / 选择高频)

FF 算法的效率取决于怎么选增广路径

策略 选法 增广次数上限 单次找路 总复杂度
任意路径 DFS/随便 \(O(f)\)\(f\)=最大流值) \(O(\|E\|)\) \(O(f \cdot \|E\|)\)
最大瓶颈路径 改造 Dijkstra \(O(\|E\| \log cap_{\max})\) \(O(\|E\| \log \|V\|)\) \(O(\|E\|^2 \log \|V\|)\)
最短路径(BFS) BFS \(O(\|E\| \cdot \|V\|)\) \(O(\|E\|)\) \(O(\|E\|^2 \|V\|)\)

↑ 第三行就是 Edmonds-Karp 算法——FF 的 BFS 实现版本,复杂度与 \(f\) 无关,保证多项式时间终止。

最坏情况举例

边容量为 \(10^6\) 时,任意路径策略可能增广 \(2 \times 10^6\) 次——每次只增 1!

特殊情况

若每个 \(v \notin \{s, t\}\) 要么只有一条容量 1 的入边,要么只有一条容量 1 的出边,则 \(T = O(|E| \cdot |V|^{1/2})\)

最小费用流(Min-Cost Flow)

所有最大流中找总费用最小的——每条边附带单位流量费用。


§5 最小生成树(Minimum Spanning Tree)

考点 9:定义与基本性质(填空必考)

生成树:图 \(G\) 的一棵树,包含 \(G\)全部顶点部分边

最小生成树(MST):边权和最小的生成树。

性质 内容
边数 恰为 \(\|V\| - 1\)
结构 树(无环),覆盖所有顶点
存在性 \(G\) 必须连通
加边性质 向 MST 加入任意一条非树边 → 必形成唯一一个环

贪心约束(解题前默念):

  1. 只能用图中已有的边;
  2. 必须恰好 \(|V| - 1\) 条边;
  3. 不能选会成环的边。

考点 6:Prim 算法 —— 生长一棵树

与 Dijkstra 几乎一致,区别仅在更新公式(用边权本身,不累加)。

解题步骤(手算模板)

1. 任选起点 \(s\),加入 \(T\)
2. 在所有"一端在 \(T\) 内、一端在 \(T\) 外"的边中选权值最小的边 \((u, v)\)
3.\(v\) 和边 \((u, v)\) 加入 \(T\)
4. 回到步骤 2,直到 \(T\) 中有 \(|V|\) 个顶点。

void Prim(Graph G, Vertex s)
{
    将 s 加入树 T;
    while (T 中顶点数 < |V|) {
        在所有一端在 T 内、一端在 T 外的边中,
        选权值最小的边 (u, v);
        将 v 加入 T,将边 (u, v) 加入 T;
    }
}

复杂度(与 Dijkstra 同):二叉堆实现为 \(O(|E| \log |V|)\)

易错点

Prim 维护的是一棵正在长大的树——任何时刻只有一个连通块。和 Kruskal 的"森林"对照记忆。


考点 7:Kruskal 算法 —— 维护一个森林

思想:边按权值从小到大排序,依次考察,不成环就加入。

解题步骤(手算模板)

1. 所有边按权值升序排列。
2. 取出当前最小边 \((v, w)\)
3.并查集判:\(v\)\(w\) 是否已在同一连通块?
 • 否 → 加入 \(T\),合并 \(v\)\(w\) 所在集合(Union)。
 • 是 → 丢弃。
4. 重复直到 \(T\) 中已有 \(|V| - 1\) 条边,或边集空。
5. 若结束时仍不足 \(|V| - 1\) 条边 → 报错"No spanning tree"(图不连通)。

void Kruskal(Graph G)
{   T = { };
    while (T contains less than |V| - 1 edges
           && E is not empty) {
        choose a least cost edge (v, w) from E;  /* DeleteMin */
        delete (v, w) from E;
        if ((v, w) does not create a cycle in T)
            add (v, w) to T;     /* Union / Find */
        else
            discard (v, w);
    }
    if (T contains fewer than |V| - 1 edges)
        Error("No spanning tree");
}

复杂度拆解

\[ T = O(|E| \log |E|) \]
子步骤 数据结构 复杂度
排序 / 取最小边 优先队列(DeleteMin) \(O(\log \|E\|)\) 每次
判环 / 合并 并查集(Union / Find) 近似 \(O(1)\) 每次

易错点

"不成环"必须用并查集判,不能用 visited 数组——Kruskal 中点是分散的多个集合。


考点 8:Prim vs Kruskal 横向对比

维度 Prim Kruskal
视角 长一棵树 维护一片森林
数据结构 二叉堆(点) 并查集 + 优先队列(边)
复杂度 \(O(\|E\| \log \|V\|)\) \(O(\|E\| \log \|E\|)\)
适用 稠密图(边多) 稀疏图(边少)
类比 类似 Dijkstra 类似排序 + 并查集

速记

  • 想问"和 Dijkstra 像吗?"→ Prim。
  • 想问"用并查集吗?"→ Kruskal。