一、流网络

流网络:有向图 G = (V, E),每条边有容量 c(u,v) ≥ 0,指定源点 s汇点 t

f 是一个函数,满足:

  1. 容量约束0 ≤ f(u,v) ≤ c(u,v)
  2. 流守恒:对每个 v ≠ s, tΣ 流入 = Σ 流出

流的值 |f| = 从 s 流出的净流量最大流问题:求 |f| 最大的流。

        ┌─12─▶ v₁ ──20──▶ v₃ ─┐
      16│      │  ╲            │ 4
    s ──┘      4    9          └──▶ t
      13│      ▼     ╲        7 ▲
        └────▶ v₂ ────────▶ v₄ ─┘
                     14

直觉模型:水管网络、公路运力、网络带宽。但网络流真正的威力在于它能建模大量看起来毫不相关的问题——这是本讲的重点。


二、残量网络与增广路

残量网络

给定流 f,残量容量 定义为:

c_f(u,v) = c(u,v) − f(u,v)      正向:还能推多少
c_f(v,u) = f(u,v)               ⭐ 反向:可以"撤销"多少

反向边是全部的关键。 它让算法可以"后悔"——如果之前的选择不好,后面的增广路可以通过反向边把流"退回来",重新路由。

容量 10,已流 6:

正向残量 4:  u ──4──▶ v          还能再推 4
反向残量 6:  u ◀──6─── v         可以退回 6

没有反向边的贪心是错的

        s ──1──▶ a ──1──▶ t
        │        │        ▲
        1        1        1
        │        ▼        │
        └─────▶  b  ──────┘

五条边容量全是 1:s→a、s→b、a→b、a→t、b→t
真正的最大流 = 2(走 s→a→t 与 s→b→t)

❌ 无反向边:若先选中 s→a→b→t(推 1),则 s→a、a→b、b→t 全部饱和。
   此后从 s 只能走 s→b,而 b 的唯一出边 b→t 已满 ⟹ 卡死,只得到 1。
✓ 有反向边:第二条增广路 s→b→(沿 a→b 的反向边退回)→a→t 再推 1 ⟹ 总流 2。
   这一步的实际效果是「把原先经 a→b 的那一单位改道成 a→t」。

增广路

增广路:残量网络中一条从 s 到 t 的路径。沿它推流量 = 路径上最小残量容量。

type FlowEdge struct {
    To, Rev int // Rev 是反向边在 adj[To] 中的下标
    Cap     int
}

type FlowNetwork struct {
    n   int
    adj [][]FlowEdge
}

func (g *FlowNetwork) AddEdge(u, v, cap int) {
    g.adj[u] = append(g.adj[u], FlowEdge{To: v, Rev: len(g.adj[v]), Cap: cap})
    g.adj[v] = append(g.adj[v], FlowEdge{To: u, Rev: len(g.adj[u]) - 1, Cap: 0}) // 反向边初始容量 0
}

“成对存边"的技巧:正反两条边相邻存放,推流时 Cap -= d 同时 反向边.Cap += d。这是网络流实现的标准写法。


三、最大流最小割定理

割 (S, T):把顶点分成两部分,s ∈ S,t ∈ T。 割的容量 c(S,T) = Σ_{u∈S, v∈T} c(u,v)只算从 S 到 T 的边)。 最小割:容量最小的割。

引理(弱对偶):任意流的值 ≤ 任意割的容量。

证明:所有从 s 到 t 的流量都必须跨过这个割,而跨割的容量上限就是 c(S,T)。∎

⭐ 最大流最小割定理(Ford-Fulkerson, 1956)

以下三个命题等价:

  1. f 是最大流
  2. 残量网络中不存在增广路
  3. 存在某个割 (S,T) 使得 |f| = c(S,T)

因此:最大流的值 = 最小割的容量。

证明(循环论证 1⟹2⟹3⟹1)

(1 ⟹ 2):若有增广路,就能增大流,与最大性矛盾。

(2 ⟹ 3):设 S = 残量网络中从 s 可达的所有顶点,T = V − S。因为无增广路,t ∉ S,所以 (S,T) 是一个合法的割。对任意 u ∈ S、v ∈ T:

  • 边 (u,v) 必然满流f(u,v) = c(u,v)),否则残量 > 0,v 就可达了
  • 边 (v,u) 必然零流f(v,u) = 0),否则反向残量 > 0,v 也可达

因此 |f| = c(S,T)

(3 ⟹ 1):由弱对偶,|f| ≤ 任意割。既然 |f| = c(S,T),f 达到了上界,必是最大流。∎

这是组合优化中最优美的定理之一,也是「线性规划对偶性」在图论中的具体化身。它把一个最大化问题和一个最小化问题绑在了一起。

求最小割:跑完最大流后,残量网络中从 s 可达的顶点集合就是 S,割边就是从 S 指向 T 的原图边。


四、算法

Edmonds-Karp:用 BFS 找增广路

func (g *FlowNetwork) MaxFlow(s, t int) int {
    flow := 0
    for {
        // BFS 找最短增广路(边数最少)
        parent := make([]int, g.n)  // parent[v] = 到达 v 的边在 adj[u] 中的下标
        pv := make([]int, g.n)      // pv[v] = 前驱顶点
        for i := range parent { pv[i] = -1 }
        pv[s] = s
        queue := []int{s}

        for len(queue) > 0 && pv[t] == -1 {
            u := queue[0]
            queue = queue[1:]
            for i, e := range g.adj[u] {
                if e.Cap > 0 && pv[e.To] == -1 {
                    pv[e.To], parent[e.To] = u, i
                    queue = append(queue, e.To)
                }
            }
        }
        if pv[t] == -1 {
            return flow // 无增广路,结束
        }

        // 找瓶颈
        bottleneck := math.MaxInt
        for v := t; v != s; v = pv[v] {
            bottleneck = min(bottleneck, g.adj[pv[v]][parent[v]].Cap)
        }
        // 沿路径推流
        for v := t; v != s; v = pv[v] {
            e := &g.adj[pv[v]][parent[v]]
            e.Cap -= bottleneck
            g.adj[v][e.Rev].Cap += bottleneck // ⭐ 反向边加上
        }
        flow += bottleneck
    }
}

复杂度 O(V·E²)

为什么用 BFS 而不是 DFS?

⚠️ 朴素 Ford-Fulkerson(DFS 任意找增广路)在容量是无理数时可能不终止,在整数容量下复杂度是 O(E · |f|)*——与流值有关,不是多项式

              C                 C
        s ────────▶ a ────────▶ t
        │           │           ▲
        │C         1│           │C
        │           ▼           │
        └────────▶  b ──────────┘

C = 1 000 000,唯独中间那条 a→b 的容量是 1
最大流 = 2C = 2×10⁶(走 s→a→t 与 s→b→t,各 C)

若每次都挑中长度为 3 的那条路:
  第 1 次 s→a→b→t          推 1(a→b 饱和)
  第 2 次 s→b→(反向)a→t    推 1(a→b 退回 0)
  两条交替往复……每次只推进 1 单位 ⟹ 共需 2×10⁶ 次增广
而 BFS 只挑长度为 2 的最短路,2 次增广(各 C)就结束了。

Edmonds-Karp 用 BFS 选最短增广路,可以证明增广次数是 O(V·E),与容量无关,得到 O(V·E²)。这是"选择顺序影响复杂度"的经典案例。

Dinic 算法

两个优化叠加

  1. 分层图(level graph):BFS 给每个顶点标记到 s 的层数,只允许走 level[v] = level[u] + 1 的边。
  2. 阻塞流(blocking flow):在分层图上用 DFS 一次找出多条增广路,直到没有 s→t 的路。
  3. 当前弧优化:记录每个顶点已经尝试到第几条边,避免重复扫描死路。
复杂度:O(V² · E)                    一般图
       O(E · √V)                    ⭐ 单位容量图(如二分匹配)
       O(V^(2/3) · E)               单位容量且无重边

Dinic 是实践中的标准选择:实现难度中等,实测远快于 Edmonds-Karp,在多数场景下表现接近线性。竞赛和工程中"网络流"基本就是指 Dinic。


五、二分匹配

这是网络流最重要的应用,也是最能体现"归约"威力的例子。

二分图最大匹配:二分图 G = (L ∪ R, E),求最大的边集 M,使 M 中任意两条边不共享顶点。

归约成最大流

        ┌──1──▶ l₁ ──▶ r₁ ──1──┐
        │        ╲   ╱          │
   s ───┼──1──▶ l₂ ─╳─ r₂ ──1───┼──▶ t
        │        ╱   ╲          │
        └──1──▶ l₃ ──▶ r₃ ──1──┘

  ① s 向每个左顶点连容量 1 的边
  ② 原图的每条边 (l,r) 连容量 1(或 ∞)
  ③ 每个右顶点向 t 连容量 1 的边
  ⟹ 最大流的值 = 最大匹配的大小

为什么对? 容量 1 保证每个顶点最多被用一次(对应匹配的定义),流的整数性定理保证最大流可以取到整数值,每条流量为 1 的路径对应一条匹配边。

复杂度:Dinic 在单位容量图上是 O(E√V),这也是 Hopcroft-Karp 算法的复杂度(后者是专门为二分匹配设计的,本质是同一个思路)。

⚠️ 简单实现可以用 匈牙利算法(对每个左顶点找增广路),O(V·E),代码只有 20 行,小规模问题足够。

⭐ König 定理与三个等价问题

König 定理(二分图)最大匹配数 = 最小顶点覆盖数

最小顶点覆盖:选最少的顶点,使每条边至少有一个端点被选中。

由此还能推出(设二分图有 n 个顶点):

最大独立集 = n − 最大匹配
最小路径覆盖(DAG)= n − 拆点二分图的最大匹配
最小边覆盖 = n − 最大匹配   (无孤立点时)

这组等价关系的实用价值极大:很多问题表面上是"选最多互不冲突的东西”(独立集)或"用最少的东西覆盖全部"(覆盖),在二分结构下它们都归结为一次最大匹配。

⚠️ 注意"二分"这个前提:一般图的最大独立集和最小顶点覆盖都是 NP-完全的(第 34 讲)。二分结构是让它们变简单的唯一原因。


六、建模:把问题变成流

网络流的真正难点不是算法(照抄 Dinic 即可),而是建图

问题 建模方式
二分匹配 s→左,左→右,右→t,容量全 1
多源多汇 加超级源 S 连所有源、超级汇 T 连所有汇
顶点容量限制 拆点:v 拆成 v_in → v_out,中间边容量 = 顶点容量
最小割 = 二元选择 每个物品要么归 S 要么归 T,割边代价 = 分开的代价
项目选择(最大权闭合子图) 正收益点连 s,负收益点连 t,依赖关系连 ∞ 边;答案 = 正收益和 − 最小割
最小路径覆盖(DAG) 每个点拆成出点和入点,做二分匹配
有上下界的流 转化成无上下界的可行流问题
最小费用最大流 每条边加费用,用 Bellman-Ford/SPFA 找最短增广路

“拆点"和"最小割 = 二元划分"是两个最常用的建模技巧。特别是后者:

当一个问题的形式是"每个元素必须二选一,某些组合会产生代价"时,往往可以建成最小割。 图像分割(前景/背景)、项目选择(做/不做)、任务分配(机器 A / 机器 B)都是这个模式。


七、复杂度汇总

算法 复杂度 备注
Ford-Fulkerson (DFS) O(E·|f*|) ⚠️ 非多项式,可能不终止
Edmonds-Karp O(V·E²) BFS 找最短增广路
Dinic O(V²·E) 实践首选
Dinic(单位容量) O(E√V) 二分匹配
Push-Relabel O(V³) 或 O(V²√E) 理论快,实现复杂
匈牙利算法(二分匹配) O(V·E) 代码最短
Hopcroft-Karp O(E√V) 二分匹配专用
最小费用最大流(SPFA) O(V·E·f) 每轮找最短路增广

随堂自测

  1. 流必须满足哪两个约束?流的值如何定义?
  2. 残量网络中的反向边有什么作用?给出一个"没有反向边就得不到最优解"的例子。
  3. 陈述最大流最小割定理的三个等价命题,并完成 (2 ⟹ 3) 的证明。
  4. 跑完最大流后,如何在 O(V+E) 内找出最小割的具体边集?
  5. 为什么朴素 Ford-Fulkerson 的复杂度 O(E·|f*|) 不算多项式?构造一个使它需要百万次增广的图。
  6. Edmonds-Karp 用 BFS 而非 DFS,为什么这一个改动就把复杂度变成了多项式?
  7. Dinic 的两个核心优化是什么?为什么它在单位容量图上是 O(E√V)?
  8. 把二分图最大匹配归约成最大流,说明容量设为 1 保证了什么。
  9. 陈述 König 定理,并写出最大独立集与最大匹配的关系。为什么这个关系只对二分图成立?
  10. 什么是"拆点"技巧?它解决什么建模需求?
  11. 什么样的问题适合建模成最小割?给出一个例子。