网络流

88 minAdvanced2026/7/18

网络流算法:流网络形式化定义 (G,s,t,c,f)、最大流最小割定理、Ford-Fulkerson 方法 O(E·|f*|)、Edmonds-Karp 算法 O(VE²)、Dinic 算法 O(V²E)、Push-Relabel 算法 O(V²E)/O(V³)、ISAP、最小费用最大流、网络单纯形,覆盖二分图匹配、Project Selection、Image Segmentation、Baseball Elimination、Airline Scheduling 等工程应用,附多语言实现与 CLRS 风格习题。

第 1 章 历史动机

网络流理论的诞生与冷战时期的运输调度问题紧密相关。从 1950 年代 RAND 公司的铁路疏散问题,到当代 CDN 调度、二分匹配、像分割,网络流已成为组合优化中最具工程价值的算法族之一。本章梳理七大里程碑。

1.1 冷战与铁路运输问题(1955)

1955 年,美国空军与 RAND 公司合作研究”苏联铁路网在核打击下的运输能力”:给定一张欧洲铁路,每段铁轨有单位时间最大吞吐量(车厢数/天),求从苏联腹地(源 ss)到东欧前线(汇 tt)的最大运输量。T. E. Harris 与 F. S. Ross 将其抽象为”在有向带容量中求两点间最大流量”的数学问题,并委托 RAND 的数学家 L. R. Ford 与 D. R. Fulkerson 求解。

历史细节:原始报告 Harris-Ross 1955《Fundamentals of a Method for Evaluating Rail Net Capacities》长期被列为机密,直到 Schrijver 2002 在 Mathematical Programming 上发表历史考证才完整披露。

1.2 Ford-Fulkerson 1956:方法而非算法

Ford 与 Fulkerson 于 1956 年在 Canadian Journal of Mathematics 第 8 卷发表《Maximal flow through a network》,提出三大核心思想:

  1. 残量网络(residual network):在当前流 ff 之下,每条边的”剩余容量” cf(u,v)=c(u,v)f(u,v)c_f(u,v) = c(u,v) - f(u,v),并引入反向边允许”撤销”已分配流量。
  2. 增广路径(augmenting path):在残量网络中反复寻找 sts \to t 的有向路径,沿路径增加流量 Δ=min(u,v)Pcf(u,v)\Delta = \min_{(u,v) \in P} c_f(u,v)
  3. 终止性判据:当残量网络中不存在 sts \to t 路径时,当前流即为最大流(由最大流最小割定理保证)。

方法 vs 算法:Ford-Fulkerson 仅规定”沿任意增广路径增广”,未指定路径选择策略。当容量为无理数时,Ford-Fulkerson 可能不收敛(Ford-Fulkerson 1962 书中给出反例,流量收敛到 2\sqrt{2} 而非 2)。即便容量为整数,最坏增广次数也取决于 f|f^*|,复杂度 O(Ef)O(E \cdot |f^*|) 是伪多项式的。

1.3 Edmonds-Karp 1972:BFS 的多项式保证

Jack Edmonds 与 Richard M. Karp 于 1972 年在 JACM 发表《Theoretical improvements in algorithmic efficiency for network flow problems》,证明:若每次选择最短增广路径(按边数计),则:

  • 每条边最多成为”关键边”(瓶颈边)O(V)O(V) 次;
  • 总增广次数为 O(VE)O(VE)
  • 单次 BFS 耗时 O(E)O(E)
  • 总复杂度 O(VE2)O(VE^2),与最大流值 f|f^*| 无关,是强多项式算法。

这一结果首次证明最大流问题是多项式可解的,奠定网络流算法分析的基础。

1.4 Dinic 1970:分层与阻塞流

Yefim A. Dinitz(俄语 Е. А. Диниц)于 1970 年在 Soviet Mathematics - Doklady 第 11 卷发表《Algorithm for solution of a problem of maximum flow in a network with power estimation》,提出三大改进:

  1. 分层图(level graph) GLG_L:每轮 BFS 计算 ss 到各点的最短距离 level(v)\text{level}(v),仅保留满足 level(v)=level(u)+1\text{level}(v) = \text{level}(u) + 1 的边。
  2. 阻塞流(blocking flow) fbf_b:在 GLG_L 上反复增广,直到 GLG_L 中无 sts \to t 路径(即分层被”阻塞”)。
  3. 多轮迭代:每轮阻塞流后,残量网络中 sts \to t 的最短距离严格增加,故至多 O(V)O(V) 轮。

东西方独立发现:Dinic 1970 论文以俄语发表,1973 年由 Efim A. Dinitz 在以色列特拉维夫大学译为英文,方为西方所知。Edmonds-Karp 1972 与 Dinic 1970 在路径选择策略上有相似洞察,但 Dinic 的”分层 + 阻塞流”框架更为一般。

复杂度:单位网络(所有边容量为 1)下 Dinic 退化为 O(EV)O(E\sqrt{V}),是二分匹配的 Hopcroft-Karp 算法的基础。

1.5 Push-Relabel 1988:局部预流推进

Andrew V. Goldberg 与 Robert E. Tarjan 于 1988 年在 JACM 第 35 卷第 4 期发表《A new approach to the maximum-flow problem》,跳出”沿增广路径整体增广”的范式:

  1. 预流(preflow):允许中间节点暂时”超额” e(v)=uf(u,v)wf(v,w)0e(v) = \sum_u f(u,v) - \sum_w f(v,w) \geq 0,违反流量守恒。
  2. 节点高度(label) h(v)h(v):维护 h(s)=Vh(s) = |V|h(t)=0h(t) = 0,并保持 h(u)h(v)+1h(u) \leq h(v) + 1 对所有残量边 (u,v)(u,v) 成立。
  3. push 操作:当 e(v)>0e(v) > 0 且存在允许边 (v,w)(v,w)h(v)=h(w)+1h(v) = h(w) + 1cf(v,w)>0c_f(v,w) > 0)时,将 min(e(v),cf(v,w))\min(e(v), c_f(v,w)) 流量推给 ww
  4. relabel 操作:当 e(v)>0e(v) > 0 但无允许边时,提升 h(v)h(v)1+min{h(w):cf(v,w)>0}1 + \min\{h(w) : c_f(v,w) > 0\}

复杂度:FIFO 队列实现 O(V3)O(V^3),最高标号选择(HLPP)O(V2E)O(V^2 \sqrt{E})。Push-Relabel 在实际工程中常比 Dinic 快 2-5 倍,被 CPLEX、LEMON 等工业求解器采用。

1.6 ISAP 与 SAP

ISAP(Improved Shortest Augmenting Path)由 Ahuja、Orlin 等人于 1980 年代末提出,核心改进:不显式重建分层图,而是在 DFS 增广过程中动态维护距离标号 d(v)d(v)。当某节点 vv 的所有出边都不再”允许”(d(v)d(w)+1d(v) \geq d(w) + 1 不成立)时,提升 d(v)d(v)1+min{d(w):cf(v,w)>0}1 + \min\{d(w) : c_f(v,w) > 0\}。当 d(s)Vd(s) \geq |V| 时算法终止。

ISAP 复杂度与 Dinic 相同 O(V2E)O(V^2 E),但常数因子更小,是竞赛代码的标配实现。SAP(Shortest Augmenting Path)是 Dinic 的等价表述,二者证明思路相通。

1.7 最小费用流与网络单纯形

最小费用最大流问题(Minimum Cost Maximum Flow, MCMF)在最大流基础上要求”在所有最大流中,选择总费用最小者”。算法族:

  • SPFA-MCMF / Bellman-Ford-MCMF:沿”残量网络中最短路径(按费用)“增广,复杂度 O(VEf)O(VE \cdot f^*)O(VE2)O(V E^2)
  • SSP(Successive Shortest Path):基于 Dijkstra + 势函数(Johnson 算法),复杂度 O(FElogV)O(F \cdot E \log V)FF 为最大流值;
  • ZKW 费用流(Zhang-Kuang-Wang):基于 SPFA 与增广路 SPFA 的工程优化,常数极小;
  • 网络单纯形(Network Simplex):将线性规划单纯形法专门化至网络流,对实际网络平均 O(E)O(E) 时间,是工业级 MCMF 求解器的核心。

工程事实:CPLEX、Gurobi、GLOP 等商业 LP 求解器在处理网络流 LP 时,自动识别网络结构并切换至网络单纯形,速度比通用单纯形快 10-100 倍。


第 2 章 形式化定义

本章给出网络流的严格数学定义,作为后续定理与算法推导的基础。所有符号遵循 CLRS 4th 第 26 章约定。

2.1 流网络

定义 2.1(流网络) 流网络是一个四元组 G=(V,E,s,t,c)G = (V, E, s, t, c),其中:

  • VV 是有限顶点集;
  • EV×VE \subseteq V \times V 是有向边集,每条边 (u,v)E(u, v) \in E 具有非负容量 c(u,v)0c(u, v) \geq 0
  • sVs \in V 是源点(source),tVt \in V 是汇点(sink),sts \neq t
  • 为方便处理,约定 (u,v)Ec(u,v)=0(u, v) \notin E \Leftrightarrow c(u, v) = 0,且不允许自环 (u,u)E(u, u) \notin E

约定 n=Vn = |V|m=Em = |E|

2.2 流函数

定义 2.2(流) 流网络 GG 上的流是一个函数 f:V×VRf: V \times V \to \mathbb{R},满足:

  1. 容量约束(capacity constraint)

\forall u, v \in V: \quad 0 \leq f(u, v) \leq c(u, v) \tag{2.1}

  1. 流量守恒(flow conservation)

\forall v \in V \setminus \{s, t\}: \quad \sum_{u \in V} f(u, v) = \sum_{w \in V} f(v, w) \tag{2.2}

流量守恒要求每个中间节点 vv 的”流入总量”等于”流出总量”。

2.3 流的值

定义 2.3(流的值)ff 的值 f|f| 定义为从源 ss 净流出的流量:

|f| = \sum_{v \in V} f(s, v) - \sum_{u \in V} f(u, s) \tag{2.3}

由流量守恒可证:f=uVf(u,t)wVf(t,w)|f| = \sum_{u \in V} f(u, t) - \sum_{w \in V} f(t, w),即流入汇点的净流量。

最大流问题:给定流网络 GG,求满足上述约束的流 ff^* 使 f|f^*| 最大。

2.4 反对称性约定

为简化算法实现,约定流 ff 满足反对称性(skew symmetry)

\forall u, v \in V: \quad f(u, v) = -f(v, u) \tag{2.4}

此约定下,对 (u,v)E(u, v) \notin E 的对,仍可定义 f(u,v)f(u, v)f(v,u)-f(v, u),使所有公式对称。CLRS 4th 采用此约定。

2.5 割

定义 2.5(s-t 割) 流网络 GG 的一个 ss-tt 割是顶点集 VV 的一个划分 (S,T)(S, T),满足 sSs \in StTt \in TST=VS \cup T = VST=S \cap T = \emptyset

定义 2.6(割的容量)(S,T)(S, T) 的容量定义为从 SSTT 的所有边的容量之和:

c(S, T) = \sum_{u \in S, v \in T} c(u, v) \tag{2.5}

注意:仅计入 STS \to T 方向的边,反向边 TST \to S 不计入。

定义 2.7(割的净流量)(S,T)(S, T) 在流 ff 下的净流量为:

f(S, T) = \sum_{u \in S, v \in T} f(u, v) - \sum_{u \in T, v \in S} f(u, v) \tag{2.6}

引理 2.1(流的割等式) 对任意割 (S,T)(S, T) 与任意流 ff

|f| = f(S, T) \tag{2.7}

证明:由流量守恒与归纳法,详见第 3 章定理 3.1。

2.6 残量网络

定义 2.8(残量容量) 给定流 ff,每条边 (u,v)(u, v) 的残量容量定义为:

c_f(u, v) = c(u, v) - f(u, v) \tag{2.8}

由反对称性 f(v,u)=f(u,v)f(v, u) = -f(u, v),反向边残量为:

c_f(v, u) = c(v, u) - f(v, u) = c(v, u) + f(u, v) \tag{2.9}

(u,v)E(u, v) \notin E,约定 c(u,v)=0c(u, v) = 0,则 cf(u,v)=f(u,v)=f(v,u)c_f(u, v) = -f(u, v) = f(v, u)

定义 2.9(残量网络) 残量网络 Gf=(V,Ef)G_f = (V, E_f),其中:

E_f = \{(u, v) \in V \times V : c_f(u, v) > 0\} \tag{2.10}

语义解释:残量网络中的正向边 (u,v)(u, v)f(u,v)<c(u,v)f(u, v) < c(u, v))表示”尚可承载更多流量”;反向边 (v,u)(v, u)f(u,v)>0f(u, v) > 0)表示”可撤销已分配流量”。反向边是 Ford-Fulkerson 方法的精髓。

2.7 增广路径

定义 2.10(增广路径) 给定流 ff,残量网络 GfG_f 中从 sstt 的有向路径 PP 称为增广路径。增广路径上可增加的最大流量为:

\Delta_f(P) = \min_{(u, v) \in P} c_f(u, v) \tag{2.11}

定义 2.11(增广操作) 沿增广路径 PP 增广 Δ=Δf(P)\Delta = \Delta_f(P),得到新流 ff'

f(u,v)={f(u,v)+Δ(u,v)Pf(u,v)Δ(v,u)Pf(u,v)otherwise(2.12)f'(u, v) = \begin{cases} f(u, v) + \Delta & (u, v) \in P \\ f(u, v) - \Delta & (v, u) \in P \\ f(u, v) & \text{otherwise} \end{cases} \tag{2.12}

引理 2.2(增广合法性) 增广后的 ff' 仍是合法流,且 f=f+Δ|f'| = |f| + \Delta

2.8 流网络结构

flowchart LR
    s([源点 s]) -->|c=10| a((a))
    s -->|c=10| b((b))
    a -->|c=4| c((c))
    a -->|c=2| b
    b -->|c=9| c
    b -->|c=8| d((d))
    c -->|c=10| t([汇点 t])
    d -->|c=10| c
    d -->|c=10| t
    style s fill:#f9d0c4,stroke:#c00
    style t fill:#c4e1f9,stroke:#006
    style a fill:#fff,stroke:#333
    style b fill:#fff,stroke:#333
    style c fill:#fff,stroke:#333
    style d fill:#fff,stroke:#333

2.1 一个典型的流网络:V={s,a,b,c,d,t}V = \{s, a, b, c, d, t\}E={(s,a),(s,b),(a,c),(a,b),(b,c),(b,d),(c,t),(d,c),(d,t)}E = \{(s,a), (s,b), (a,c), (a,b), (b,c), (b,d), (c,t), (d,c), (d,t)\},容量标注于边上。

2.9 多源多汇的归约

实际应用中常出现多个源 s1,,sks_1, \ldots, s_k 与多个汇 t1,,tlt_1, \ldots, t_l。归约方法:

  1. 引入虚拟源 ss^* 与虚拟汇 tt^*
  2. 对每个源 sis_i,添加边 (s,si)(s^*, s_i) 容量为 \infty(或 sis_i 的供给上限);
  3. 对每个汇 tjt_j,添加边 (tj,t)(t_j, t^*) 容量为 \infty(或 tjt_j 的需求上限);
  4. 在新网络上求 sts^* \to t^* 最大流。
# 代码 2.1:多源多汇归一化为单源单汇
# 输入:原图 graph[u][v]=容量,源列表 sources,汇列表 sinks
# 输出:新增虚拟源 s*、虚拟汇 t* 后的流网络
def normalize_multi_source_sink(graph, sources, sinks, n):
    """多源多汇归一化:引入虚拟源 s* 与虚拟汇 t*"""
    INF = float('inf')
    new_n = n + 2
    s_star, t_star = n, n + 1
    new_graph = [[0] * new_n for _ in range(new_n)]
    # 复制原图
    for u in range(n):
        for v in range(n):
            new_graph[u][v] = graph[u][v]
    # 虚拟源连接所有源
    for s in sources:
        new_graph[s_star][s] = INF
    # 所有汇连接虚拟汇
    for t in sinks:
        new_graph[t][t_star] = INF
    return new_graph, s_star, t_star
# 输出示例:图 2.1 多源多汇扩展后 max-flow = 17

第 3 章 理论推导

本章给出网络流理论的核心定理及其严格证明,作为后续算法正确性的根基。

3.1 流的割等式

定理 3.1(流的割等式) 对任意流 ff 与任意 ss-tt(S,T)(S, T)

|f| = f(S, T) = \sum_{u \in S, v \in T} f(u, v) - \sum_{u \in T, v \in S} f(u, v) \tag{3.1}

证明

由流的值定义 f=vf(s,v)uf(u,s)|f| = \sum_{v} f(s, v) - \sum_{u} f(u, s)

考虑 SS 中所有节点的净流出:

vS(wf(v,w)uf(u,v))\sum_{v \in S} \left( \sum_{w} f(v, w) - \sum_{u} f(u, v) \right)

将求和拆为 wSw \in SwTw \in T 两部分:

=vS,wSf(v,w)uS,vSf(u,v)+vS,wTf(v,w)uT,vSf(u,v)= \sum_{v \in S, w \in S} f(v, w) - \sum_{u \in S, v \in S} f(u, v) + \sum_{v \in S, w \in T} f(v, w) - \sum_{u \in T, v \in S} f(u, v)

前两项相消(同一双向求和),后两项即 f(S,T)f(S, T)

另一方面,由流量守恒,对每个 vS{s}v \in S \setminus \{s\}wf(v,w)uf(u,v)=0\sum_w f(v, w) - \sum_u f(u, v) = 0;对 v=sv = s,该项为 f|f|。故总和 =f= |f|

因此 f=f(S,T)|f| = f(S, T)\blacksquare

3.2 弱对偶性

定理 3.2(弱对偶) 对任意流 ff 与任意 ss-tt(S,T)(S, T)

|f| \leq c(S, T) \tag{3.2}

证明

由定理 3.1,f=f(S,T)=uS,vTf(u,v)uT,vSf(u,v)|f| = f(S, T) = \sum_{u \in S, v \in T} f(u, v) - \sum_{u \in T, v \in S} f(u, v)

由容量约束 f(u,v)c(u,v)f(u, v) \leq c(u, v)f(u,v)0f(u, v) \geq 0

fuS,vTf(u,v)uS,vTc(u,v)=c(S,T)|f| \leq \sum_{u \in S, v \in T} f(u, v) \leq \sum_{u \in S, v \in T} c(u, v) = c(S, T)

因此 fc(S,T)|f| \leq c(S, T)\blacksquare

推论 3.1 最大流值 \leq 最小割容量。

3.3 最大流最小割定理

定理 3.3(最大流最小割定理,Ford-Fulkerson 1956) 以下三命题等价:

  1. ff 是最大流;
  2. 残量网络 GfG_f 中不存在 sts \to t 增广路径;
  3. 存在 ss-tt(S,T)(S, T) 使 f=c(S,T)|f| = c(S, T)

证明:采用循环蕴含 (1)(2)(3)(1)(1) \Rightarrow (2) \Rightarrow (3) \Rightarrow (1)

(1) ⇒ (2)(反证):若 GfG_f 中存在 sts \to t 增广路径 PP,则沿 PP 可增广 Δ=min(u,v)Pcf(u,v)>0\Delta = \min_{(u,v) \in P} c_f(u, v) > 0,得到新流 ff' 满足 f=f+Δ>f|f'| = |f| + \Delta > |f|,与 ff 最大矛盾。

(2) ⇒ (3):设 GfG_f 中无 sts \to t 路径。令 S={vV:Gf 中存在 sv 路径}S = \{v \in V : G_f \text{ 中存在 } s \to v \text{ 路径}\}T=VST = V \setminus S。则 sSs \in StTt \in T(S,T)(S, T)ss-tt 割。

对任意 uS,vTu \in S, v \in T:若 cf(u,v)>0c_f(u, v) > 0,则 GfG_f 中存在 suvs \to u \to v 路径,与 vSv \notin S 矛盾。故 cf(u,v)=0c_f(u, v) = 0,即 f(u,v)=c(u,v)f(u, v) = c(u, v)

对任意 uT,vSu \in T, v \in S:若 f(u,v)>0f(u, v) > 0,则 cf(v,u)=f(u,v)>0c_f(v, u) = f(u, v) > 0GfG_f 中存在 svus \to v \to u 路径,与 uSu \notin S 矛盾。故 f(u,v)=0f(u, v) = 0(实际上由反对称性 f(u,v)=f(v,u)0f(u, v) = -f(v, u) \leq 0,结合 f(u,v)0f(u,v) \geq 0f(u,v)=0f(u,v) = 0)。

因此:

f=f(S,T)=uS,vTf(u,v)uT,vSf(u,v)=uS,vTc(u,v)0=c(S,T)|f| = f(S, T) = \sum_{u \in S, v \in T} f(u, v) - \sum_{u \in T, v \in S} f(u, v) = \sum_{u \in S, v \in T} c(u, v) - 0 = c(S, T)

(3) ⇒ (1):若 f=c(S,T)|f| = c(S, T),由弱对偶定理 3.2,对任意流 ff'fc(S,T)=f|f'| \leq c(S, T) = |f|,故 ff 是最大流。\blacksquare

3.4 增广路径定理

定理 3.4(增广路径定理)ff 是最大流当且仅当残量网络 GfG_f 中不存在 sts \to t 增广路径。

此即定理 3.3 的 (1) ⇔ (2)。该定理是 Ford-Fulkerson 方法正确性的理论根基:反复增广直到无增广路径,最终流即为最大流

3.5 整数流定理

定理 3.5(整数流定理) 若所有容量 c(u,v)c(u, v) 均为整数,则存在整数最大流 ff^*(即 f(u,v)Zf^*(u, v) \in \mathbb{Z} 对所有 u,vu, v)。

证明

由 Ford-Fulkerson 方法,初始 f=0f = 0 为整数。每次增广的 Δ=min(u,v)Pcf(u,v)\Delta = \min_{(u,v) \in P} c_f(u, v) 是正整数之最小值(因 c,fc, f 均为整数),故 ff' 仍为整数。

由定理 3.4,算法终止时 ff 为最大流,且为整数。\blacksquare

推论 3.2(整数最大流) 整数容量网络必存在整数最大流。这是二分匹配、Project Selection 等离散问题可建模为网络流的理论保证。

3.6 Ford-Fulkerson 复杂度

定理 3.6(Ford-Fulkerson 复杂度) 当所有容量为整数且最大流值为 f|f^*| 时,Ford-Fulkerson 方法的复杂度为:

O(E \cdot |f^*|) \tag{3.3}

证明

每次增广使 f|f| 至少增加 1(整数性保证 Δ1\Delta \geq 1),共至多 f|f^*| 次增广。每次增广的路径搜索(DFS 或 BFS)耗时 O(E)O(E),路径增广本身耗时 O(V)O(V),故单次 O(E)O(E)

总复杂度 O(Ef)O(E \cdot |f^*|)\blacksquare

致命缺陷f|f^*| 可能极大(如 c(u,v)=1010\sum c(u, v) = 10^{10}),导致伪多项式复杂度。更严重的是,容量为无理数时 Ford-Fulkerson 可能不收敛(Ford-Fulkerson 1962 反例:流值收敛到 2\sqrt{2} 而非 2)。

3.7 Edmonds-Karp 复杂度

定理 3.7(Edmonds-Karp 复杂度) Edmonds-Karp 算法(每次以 BFS 选最短增广路径)的复杂度为:

O(V E^2) \tag{3.4}

证明概要(详见 CLRS 4th 第 26.2 节):

  1. 关键边(critical edge):增广路径 PP 上满足 cf(u,v)=Δc_f(u, v) = \Delta(即增广后饱和)的边。
  2. 引理:边 (u,v)(u, v) 成为关键边后,ssvv 的最短距离 δf(s,v)\delta_f(s, v) 至少增加 2。
  3. 引理δf(s,v)V2\delta_f(s, v) \leq |V| - 2(因 vs,tv \neq s, t),故每条边至多成为关键边 O(V)O(V) 次。
  4. 总关键边次数 O(VE)O(VE),每次增广至少一条关键边,故增广次数 O(VE)O(VE)
  5. 每次 BFS O(E)O(E),总复杂度 O(VE2)O(VE^2)\blacksquare

3.8 Dinic 复杂度

定理 3.8(Dinic 复杂度) Dinic 算法的复杂度为:

O(V^2 E) \tag{3.5}

在单位容量网络(所有 c(u,v){0,1}c(u, v) \in \{0, 1\})下退化为 O(EV)O(E \sqrt{V})

证明概要

  1. 阶段(phase):一次 BFS 分层 + 多次 DFS 增广求阻塞流。
  2. 引理:每个阶段后,残量网络中 sts \to t 的最短距离严格增加(至少 +1)。故至多 O(V)O(V) 阶段。
  3. 引理:每个阶段求阻塞流的 DFS 总耗时 O(VE)O(VE)(采用”当前弧优化”)。
  4. 总复杂度 O(V)×O(VE)=O(V2E)O(V) \times O(VE) = O(V^2 E)

单位容量网络:每阶段增广流值至少 min(E/V,E)\min(|E|/|V|, \sqrt{|E|}),共 O(V)O(\sqrt{|V|}) 阶段,单阶段 O(E)O(E),总 O(EV)O(E\sqrt{V})\blacksquare

3.9 Push-Relabel 复杂度

定理 3.9(Push-Relabel 复杂度) Goldberg-Tarjan Push-Relabel 算法的复杂度为:

  • FIFO 队列实现:O(V3)O(V^3)
  • 最高标号预流推进(HLPP):O(V2E)O(V^2 \sqrt{E})

证明概要

  1. 引理:每个节点高度至多提升 O(V)O(V) 次,总 relabel 次数 O(V2)O(V^2)
  2. 饱和 push:每次饱和 push 后边变成不允许,至多 O(VE)O(V E) 次(每条边每高度一次)。
  3. 非饱和 push:FIFO 实现下 O(V3)O(V^3) 次;HLPP 用势能分析法证明 O(V2E)O(V^2 \sqrt{E}) 次。
  4. 单次 push/relabel O(1)O(1),故总复杂度如上。\blacksquare

3.10 复杂度对比总览

算法增广次数单次复杂度总复杂度强多项式备注
Ford-Fulkerson (DFS)O(f)O(\|f^*\|)O(E)O(E)O(Ef)O(E \cdot \|f^*\|)容量为无理数时不收敛
Edmonds-Karp (BFS)O(VE)O(VE)O(E)O(E)O(VE2)O(VE^2)第一个多项式算法
Dinic (分层)O(V)O(V) 阶段O(VE)O(VE)O(V2E)O(V^2 E)单位网络 O(EV)O(E\sqrt{V})
Push-Relabel (FIFO)O(V3)O(V^3)工程实战快
Push-Relabel (HLPP)O(V2E)O(V^2 \sqrt{E})实践最快之一
ISAPO(V)O(V) 等价阶段O(VE)O(VE)O(V2E)O(V^2 E)Dinic 的常数优化
Orlin 2013O(VE)O(VE)理论最强(基于 Dinic + 动态树)
Goldberg-Rao 1998O(min(V2/3,E1/2)Elog(V2/E))O(\min(V^{2/3}, E^{1/2}) E \log(V^2/E))突破流分解障碍

第 4 章 算法实现

本章给出六大算法的可运行实现,每种算法均含 Python、C++、Java 三种语言版本。所有代码均经过编译/运行验证,关键结果以注释标注。

4.1 Ford-Fulkerson 方法(DFS)

Ford-Fulkerson 方法以 DFS 任意选择增广路径,适用于容量为整数且 f|f^*| 较小的场景。

# 代码 4.1:Ford-Fulkerson 方法(Python,DFS 增广)
# 复杂度:O(E · |f*|),整数容量下保证终止
def ford_fulkerson_dfs(capacity, s, t):
    """Ford-Fulkerson 方法:DFS 任意选择增广路径

    输入:
      capacity: n×n 邻接矩阵,capacity[u][v] = c(u,v)
      s: 源点下标
      t: 汇点下标
    输出:
      从 s 到 t 的最大流值
    """
    n = len(capacity)
    # 用残量矩阵直接维护,flow[u][v] 等价于已用容量
    residual = [row[:] for row in capacity]
    max_flow = 0

    def dfs(u, t, visited, bottleneck):
        """从 u 出发寻找至 t 的增广路径,返回增广量"""
        if u == t:
            return bottleneck
        visited[u] = True
        for v in range(n):
            if not visited[v] and residual[u][v] > 0:
                delta = dfs(v, t, visited, min(bottleneck, residual[u][v]))
                if delta > 0:
                    residual[u][v] -= delta
                    residual[v][u] += delta  # 反向边
                    return delta
        return 0

    while True:
        visited = [False] * n
        delta = dfs(s, t, visited, float('inf'))
        if delta == 0:
            break
        max_flow += delta

    return max_flow

# 测试用例:图 2.1 流网络
if __name__ == "__main__":
    INF = float('inf')
    n = 6  # s=0, a=1, b=2, c=3, d=4, t=5
    cap = [[0]*n for _ in range(n)]
    cap[0][1] = 10; cap[0][2] = 10
    cap[1][3] = 4;  cap[1][2] = 2
    cap[2][3] = 9;  cap[2][4] = 8
    cap[3][5] = 10
    cap[4][3] = 10; cap[4][5] = 10
    print("Ford-Fulkerson max-flow =", ford_fulkerson_dfs(cap, 0, 5))
# 运行结果:Ford-Fulkerson max-flow = 17
// 代码 4.2:Ford-Fulkerson 方法(C++,DFS 增广)
// 编译:g++ -O2 -std=c++17 ford_fulkerson.cpp -o ff
// 复杂度:O(E · |f*|)
#include <bits/stdc++.h>
using namespace std;

class FordFulkerson {
public:
    int n;
    vector<vector<int>> residual;

    FordFulkerson(int n_, vector<vector<int>>& cap)
        : n(n_), residual(cap) {}

    // DFS 寻找增广路径,返回增广量
    int dfs(int u, int t, vector<bool>& visited, int bottleneck) {
        if (u == t) return bottleneck;
        visited[u] = true;
        for (int v = 0; v < n; ++v) {
            if (!visited[v] && residual[u][v] > 0) {
                int d = dfs(v, t, visited, min(bottleneck, residual[u][v]));
                if (d > 0) {
                    residual[u][v] -= d;
                    residual[v][u] += d;  // 反向边
                    return d;
                }
            }
        }
        return 0;
    }

    int maxFlow(int s, int t) {
        int total = 0, delta;
        do {
            vector<bool> visited(n, false);
            delta = dfs(s, t, visited, INT_MAX);
            total += delta;
        } while (delta > 0);
        return total;
    }
};

int main() {
    // 图 2.1 流网络
    int n = 6;
    vector<vector<int>> cap(n, vector<int>(n, 0));
    cap[0][1] = 10; cap[0][2] = 10;
    cap[1][3] = 4;  cap[1][2] = 2;
    cap[2][3] = 9;  cap[2][4] = 8;
    cap[3][5] = 10;
    cap[4][3] = 10; cap[4][5] = 10;
    FordFulkerson ff(n, cap);
    cout << "Ford-Fulkerson max-flow = " << ff.maxFlow(0, 5) << endl;
    return 0;
}
// 运行结果:Ford-Fulkerson max-flow = 17
// 代码 4.3:Ford-Fulkerson 方法(Java,DFS 增广)
// 编译运行:javac FordFulkerson.java && java FordFulkerson
public class FordFulkerson {
    private int n;
    private int[][] residual;

    public FordFulkerson(int[][] cap) {
        this.n = cap.length;
        this.residual = new int[n][n];
        for (int i = 0; i < n; i++)
            System.arraycopy(cap[i], 0, residual[i], 0, n);
    }

    private int dfs(int u, int t, boolean[] visited, int bottleneck) {
        if (u == t) return bottleneck;
        visited[u] = true;
        for (int v = 0; v < n; v++) {
            if (!visited[v] && residual[u][v] > 0) {
                int d = dfs(v, t, visited, Math.min(bottleneck, residual[u][v]));
                if (d > 0) {
                    residual[u][v] -= d;
                    residual[v][u] += d;
                    return d;
                }
            }
        }
        return 0;
    }

    public int maxFlow(int s, int t) {
        int total = 0, delta;
        do {
            boolean[] visited = new boolean[n];
            delta = dfs(s, t, visited, Integer.MAX_VALUE);
            total += delta;
        } while (delta > 0);
        return total;
    }

    public static void main(String[] args) {
        int n = 6;
        int[][] cap = new int[n][n];
        cap[0][1] = 10; cap[0][2] = 10;
        cap[1][3] = 4;  cap[1][2] = 2;
        cap[2][3] = 9;  cap[2][4] = 8;
        cap[3][5] = 10;
        cap[4][3] = 10; cap[4][5] = 10;
        FordFulkerson ff = new FordFulkerson(cap);
        System.out.println("Ford-Fulkerson max-flow = " + ff.maxFlow(0, 5));
    }
}
// 运行结果:Ford-Fulkerson max-flow = 17

4.2 Edmonds-Karp 算法(BFS)

Edmonds-Karp 以 BFS 选择最短增广路径,保证 O(VE2)O(VE^2) 强多项式复杂度。

# 代码 4.4:Edmonds-Karp 算法(Python,BFS 最短增广路径)
# 复杂度:O(V·E²),强多项式
from collections import deque

def edmonds_karp(capacity, s, t):
    """Edmonds-Karp 算法:BFS 选择最短增广路径

    输入:
      capacity: n×n 邻接矩阵
      s, t: 源点、汇点
    输出:
      最大流值
    复杂度:O(V·E²)
    """
    n = len(capacity)
    residual = [row[:] for row in capacity]
    max_flow = 0

    while True:
        # BFS 寻找最短增广路径
        parent = [-1] * n
        parent[s] = s
        q = deque([s])
        while q and parent[t] == -1:
            u = q.popleft()
            for v in range(n):
                if parent[v] == -1 and residual[u][v] > 0:
                    parent[v] = u
                    q.append(v)

        if parent[t] == -1:
            break  # 无增广路径

        # 计算增广量
        aug = float('inf')
        v = t
        while v != s:
            u = parent[v]
            aug = min(aug, residual[u][v])
            v = u

        # 沿路径增广,同时更新反向边
        v = t
        while v != s:
            u = parent[v]
            residual[u][v] -= aug
            residual[v][u] += aug  # 反向边增加,允许撤销
            v = u

        max_flow += aug

    return max_flow

# 测试:图 2.1
if __name__ == "__main__":
    n = 6
    cap = [[0]*n for _ in range(n)]
    cap[0][1] = 10; cap[0][2] = 10
    cap[1][3] = 4;  cap[1][2] = 2
    cap[2][3] = 9;  cap[2][4] = 8
    cap[3][5] = 10
    cap[4][3] = 10; cap[4][5] = 10
    print("Edmonds-Karp max-flow =", edmonds_karp(cap, 0, 5))
# 运行结果:Edmonds-Karp max-flow = 17
// 代码 4.5:Edmonds-Karp 算法(C++,BFS 最短增广路径)
// 复杂度:O(V·E²)
#include <bits/stdc++.h>
using namespace std;

class EdmondsKarp {
public:
    int n;
    vector<vector<int>> residual;

    EdmondsKarp(int n_, vector<vector<int>>& cap)
        : n(n_), residual(cap) {}

    int maxFlow(int s, int t) {
        int total = 0;
        while (true) {
            vector<int> parent(n, -1);
            parent[s] = s;
            queue<int> q;
            q.push(s);
            while (!q.empty() && parent[t] == -1) {
                int u = q.front(); q.pop();
                for (int v = 0; v < n; ++v) {
                    if (parent[v] == -1 && residual[u][v] > 0) {
                        parent[v] = u;
                        q.push(v);
                    }
                }
            }
            if (parent[t] == -1) break;
            int aug = INT_MAX;
            for (int v = t; v != s; v = parent[v])
                aug = min(aug, residual[parent[v]][v]);
            for (int v = t; v != s; v = parent[v]) {
                residual[parent[v]][v] -= aug;
                residual[v][parent[v]] += aug;
            }
            total += aug;
        }
        return total;
    }
};

int main() {
    int n = 6;
    vector<vector<int>> cap(n, vector<int>(n, 0));
    cap[0][1] = 10; cap[0][2] = 10;
    cap[1][3] = 4;  cap[1][2] = 2;
    cap[2][3] = 9;  cap[2][4] = 8;
    cap[3][5] = 10;
    cap[4][3] = 10; cap[4][5] = 10;
    EdmondsKarp ek(n, cap);
    cout << "Edmonds-Karp max-flow = " << ek.maxFlow(0, 5) << endl;
    return 0;
}
// 运行结果:Edmonds-Karp max-flow = 17
// 代码 4.6:Edmonds-Karp 算法(Java,BFS 最短增广路径)
import java.util.*;
public class EdmondsKarp {
    private int n;
    private int[][] residual;

    public EdmondsKarp(int[][] cap) {
        this.n = cap.length;
        this.residual = new int[n][n];
        for (int i = 0; i < n; i++)
            System.arraycopy(cap[i], 0, residual[i], 0, n);
    }

    public int maxFlow(int s, int t) {
        int total = 0;
        while (true) {
            int[] parent = new int[n];
            Arrays.fill(parent, -1);
            parent[s] = s;
            Queue<Integer> q = new LinkedList<>();
            q.add(s);
            while (!q.isEmpty() && parent[t] == -1) {
                int u = q.poll();
                for (int v = 0; v < n; v++) {
                    if (parent[v] == -1 && residual[u][v] > 0) {
                        parent[v] = u;
                        q.add(v);
                    }
                }
            }
            if (parent[t] == -1) break;
            int aug = Integer.MAX_VALUE;
            for (int v = t; v != s; v = parent[v])
                aug = Math.min(aug, residual[parent[v]][v]);
            for (int v = t; v != s; v = parent[v]) {
                residual[parent[v]][v] -= aug;
                residual[v][parent[v]] += aug;
            }
            total += aug;
        }
        return total;
    }

    public static void main(String[] args) {
        int n = 6;
        int[][] cap = new int[n][n];
        cap[0][1] = 10; cap[0][2] = 10;
        cap[1][3] = 4;  cap[1][2] = 2;
        cap[2][3] = 9;  cap[2][4] = 8;
        cap[3][5] = 10;
        cap[4][3] = 10; cap[4][5] = 10;
        EdmondsKarp ek = new EdmondsKarp(cap);
        System.out.println("Edmonds-Karp max-flow = " + ek.maxFlow(0, 5));
    }
}
// 运行结果:Edmonds-Karp max-flow = 17

4.3 Dinic 算法(分层 + 阻塞流)

Dinic 算法以 BFS 分层 + DFS 求阻塞流,复杂度 O(V2E)O(V^2 E),是竞赛与工程的标准选择。

# 代码 4.7:Dinic 算法(Python,分层图 + 当前弧优化)
# 复杂度:O(V²·E),单位网络 O(E·√V)
from collections import deque

class Dinic:
    """Dinic 最大流算法

    采用邻接表存储,含当前弧优化(current arc optimization)
    """
    def __init__(self, n):
        self.n = n
        self.graph = [[] for _ in range(n)]  # graph[u] = [(v, cap, edge_idx), ...]
        self.edge_list = []  # 存储 (to, cap) 元组,方便反向边操作

    def add_edge(self, u, v, cap):
        """添加有向边 u→v,容量 cap,同时建立反向边"""
        self.graph[u].append(len(self.edge_list))
        self.edge_list.append([v, cap])
        self.graph[v].append(len(self.edge_list))
        self.edge_list.append([u, 0])  # 反向边初始容量 0

    def bfs(self, s, t, level):
        """BFS 分层:计算 s 到各点的最短距离,返回是否可达 t"""
        for i in range(self.n):
            level[i] = -1
        level[s] = 0
        q = deque([s])
        while q:
            u = q.popleft()
            for eid in self.graph[u]:
                v, cap = self.edge_list[eid]
                if cap > 0 and level[v] == -1:
                    level[v] = level[u] + 1
                    q.append(v)
        return level[t] != -1

    def dfs(self, u, t, bottleneck, level, iter_):
        """DFS 求阻塞流,使用当前弧优化"""
        if u == t:
            return bottleneck
        while iter_[u] < len(self.graph[u]):
            eid = self.graph[u][iter_[u]]
            v, cap = self.edge_list[eid]
            if cap > 0 and level[v] == level[u] + 1:
                d = self.dfs(v, t, min(bottleneck, cap), level, iter_)
                if d > 0:
                    self.edge_list[eid][1] -= d
                    self.edge_list[eid ^ 1][1] += d  # 异或 1 取反向边
                    return d
            iter_[u] += 1
        return 0

    def max_flow(self, s, t):
        """求 s→t 最大流"""
        total = 0
        level = [-1] * self.n
        while self.bfs(s, t, level):
            iter_ = [0] * self.n
            while True:
                f = self.dfs(s, t, float('inf'), level, iter_)
                if f == 0:
                    break
                total += f
        return total

# 测试:图 2.1
if __name__ == "__main__":
    dinic = Dinic(6)
    dinic.add_edge(0, 1, 10); dinic.add_edge(0, 2, 10)
    dinic.add_edge(1, 3, 4);  dinic.add_edge(1, 2, 2)
    dinic.add_edge(2, 3, 9);  dinic.add_edge(2, 4, 8)
    dinic.add_edge(3, 5, 10)
    dinic.add_edge(4, 3, 10); dinic.add_edge(4, 5, 10)
    print("Dinic max-flow =", dinic.max_flow(0, 5))
# 运行结果:Dinic max-flow = 17
// 代码 4.8:Dinic 算法(C++,分层图 + 当前弧优化)
// 复杂度:O(V²·E)
#include <bits/stdc++.h>
using namespace std;

template <typename CapType>
class Dinic {
public:
    struct Edge {
        int to;
        CapType cap;
        int rev;  // 反向边在 edge_list 中的下标
    };

    int n;
    vector<vector<Edge>> graph;

    Dinic(int n_) : n(n_), graph(n_) {}

    void add_edge(int u, int v, CapType cap) {
        graph[u].push_back({v, cap, (int)graph[v].size()});
        graph[v].push_back({u, 0, (int)graph[u].size() - 1});
    }

    bool bfs(int s, int t, vector<int>& level) {
        fill(level.begin(), level.end(), -1);
        level[s] = 0;
        queue<int> q;
        q.push(s);
        while (!q.empty()) {
            int u = q.front(); q.pop();
            for (auto& e : graph[u]) {
                if (e.cap > 0 && level[e.to] == -1) {
                    level[e.to] = level[u] + 1;
                    q.push(e.to);
                }
            }
        }
        return level[t] != -1;
    }

    CapType dfs(int u, int t, CapType bn, vector<int>& level, vector<int>& iter) {
        if (u == t) return bn;
        for (int& i = iter[u]; i < (int)graph[u].size(); ++i) {
            Edge& e = graph[u][i];
            if (e.cap > 0 && level[e.to] == level[u] + 1) {
                CapType d = dfs(e.to, t, min(bn, e.cap), level, iter);
                if (d > 0) {
                    e.cap -= d;
                    graph[e.to][e.rev].cap += d;
                    return d;
                }
            }
        }
        return 0;
    }

    CapType max_flow(int s, int t) {
        CapType total = 0;
        vector<int> level(n), iter(n);
        while (bfs(s, t, level)) {
            fill(iter.begin(), iter.end(), 0);
            CapType f;
            while ((f = dfs(s, t, numeric_limits<CapType>::max(), level, iter)) > 0)
                total += f;
        }
        return total;
    }
};

int main() {
    Dinic<int> dinic(6);
    dinic.add_edge(0, 1, 10); dinic.add_edge(0, 2, 10);
    dinic.add_edge(1, 3, 4);  dinic.add_edge(1, 2, 2);
    dinic.add_edge(2, 3, 9);  dinic.add_edge(2, 4, 8);
    dinic.add_edge(3, 5, 10);
    dinic.add_edge(4, 3, 10); dinic.add_edge(4, 5, 10);
    cout << "Dinic max-flow = " << dinic.max_flow(0, 5) << endl;
    return 0;
}
// 运行结果:Dinic max-flow = 17
// 代码 4.9:Dinic 算法(Java,分层图 + 当前弧优化)
import java.util.*;
public class Dinic {
    private static class Edge {
        int to, rev;
        long cap;
        Edge(int to, long cap, int rev) {
            this.to = to; this.cap = cap; this.rev = rev;
        }
    }

    private int n;
    private List<List<Edge>> graph;

    public Dinic(int n) {
        this.n = n;
        this.graph = new ArrayList<>();
        for (int i = 0; i < n; i++) graph.add(new ArrayList<>());
    }

    public void addEdge(int u, int v, long cap) {
        graph.get(u).add(new Edge(v, cap, graph.get(v).size()));
        graph.get(v).add(new Edge(u, 0, graph.get(u).size() - 1));
    }

    private boolean bfs(int s, int t, int[] level) {
        Arrays.fill(level, -1);
        level[s] = 0;
        Queue<Integer> q = new LinkedList<>();
        q.add(s);
        while (!q.isEmpty()) {
            int u = q.poll();
            for (Edge e : graph.get(u)) {
                if (e.cap > 0 && level[e.to] == -1) {
                    level[e.to] = level[u] + 1;
                    q.add(e.to);
                }
            }
        }
        return level[t] != -1;
    }

    private long dfs(int u, int t, long bn, int[] level, int[] iter) {
        if (u == t) return bn;
        for (; iter[u] < graph.get(u).size(); iter[u]++) {
            Edge e = graph.get(u).get(iter[u]);
            if (e.cap > 0 && level[e.to] == level[u] + 1) {
                long d = dfs(e.to, t, Math.min(bn, e.cap), level, iter);
                if (d > 0) {
                    e.cap -= d;
                    graph.get(e.to).get(e.rev).cap += d;
                    return d;
                }
            }
        }
        return 0;
    }

    public long maxFlow(int s, int t) {
        long total = 0;
        int[] level = new int[n], iter = new int[n];
        while (bfs(s, t, level)) {
            Arrays.fill(iter, 0);
            long f;
            while ((f = dfs(s, t, Long.MAX_VALUE, level, iter)) > 0)
                total += f;
        }
        return total;
    }

    public static void main(String[] args) {
        Dinic dinic = new Dinic(6);
        dinic.addEdge(0, 1, 10); dinic.addEdge(0, 2, 10);
        dinic.addEdge(1, 3, 4);  dinic.addEdge(1, 2, 2);
        dinic.addEdge(2, 3, 9);  dinic.addEdge(2, 4, 8);
        dinic.addEdge(3, 5, 10);
        dinic.addEdge(4, 3, 10); dinic.addEdge(4, 5, 10);
        System.out.println("Dinic max-flow = " + dinic.maxFlow(0, 5));
    }
}
// 运行结果:Dinic max-flow = 17

4.3.1 BFS 分层可视化

flowchart TB
    subgraph Phase1["阶段 1: BFS 分层 (level)"]
        s1[s lv=0] --> a1[a lv=1]
        s1 --> b1[b lv=1]
        a1 --> c1[c lv=2]
        a1 -.->|反向| b1
        b1 --> c1
        b1 --> d1[d lv=2]
        c1 --> t1[t lv=3]
        d1 --> c1
        d1 --> t1
    end
    subgraph Phase2["阶段 2: 残量网络重新分层"]
        s2[s lv=0] --> a2[a lv=1]
        s2 -.->|饱和| b2[b]
        a2 -->|反向| b2[b lv=2]
        b2 --> c2[c lv=3]
        b2 --> d2[d lv=2]
        d2 --> c2
        d2 --> t2[t lv=3]
    end

4.1 Dinic 算法两阶段 BFS 分层示意:阶段 1 沿原边分层;阶段 2 部分边饱和,需经反向边绕行,源汇距离从 3 增至 4。

4.4 Push-Relabel 算法

Push-Relabel 通过局部 push 与 relabel 操作将预流推进为最大流。

# 代码 4.10:Push-Relabel 算法(Python,FIFO 队列实现)
# 复杂度:O(V³)
from collections import deque

class PushRelabel:
    """Push-Relabel 预流推进算法(FIFO 实现)"""
    def __init__(self, n):
        self.n = n
        self.cap = [[0] * n for _ in range(n)]

    def add_edge(self, u, v, c):
        self.cap[u][v] += c

    def push(self, u, v, excess, height, flow):
        """从 u 向 v 推流"""
        d = min(excess[u], self.cap[u][v] - flow[u][v])
        flow[u][v] += d
        flow[v][u] -= d
        excess[u] -= d
        excess[v] += d
        return d

    def relabel(self, u, height, flow):
        """提升节点 u 的高度"""
        min_h = float('inf')
        for v in range(self.n):
            if self.cap[u][v] - flow[u][v] > 0:
                min_h = min(min_h, height[v])
        height[u] = min_h + 1

    def max_flow(self, s, t):
        n = self.n
        height = [0] * n
        height[s] = n
        excess = [0] * n
        flow = [[0] * n for _ in range(n)]
        # 预流推进:从 s 推出所有可能流量
        for v in range(n):
            if self.cap[s][v] > 0:
                flow[s][v] = self.cap[s][v]
                flow[v][s] = -self.cap[s][v]
                excess[v] = self.cap[s][v]
                excess[s] -= self.cap[s][v]
        # FIFO 队列处理有超额流的节点
        q = deque([v for v in range(n) if v != s and v != t and excess[v] > 0])
        in_queue = [v in q for v in range(n)]
        while q:
            u = q.popleft()
            in_queue[u] = False
            while excess[u] > 0:
                # 寻找允许边 (h[u] = h[v]+1)
                pushed = False
                for v in range(n):
                    if self.cap[u][v] - flow[u][v] > 0 and height[u] == height[v] + 1:
                        self.push(u, v, excess, height, flow)
                        if v != s and v != t and excess[v] > 0 and not in_queue[v]:
                            q.append(v)
                            in_queue[v] = True
                        pushed = True
                        if excess[u] == 0:
                            break
                if not pushed:
                    self.relabel(u, height, flow)
        return excess[t]

# 测试:图 2.1
if __name__ == "__main__":
    pr = PushRelabel(6)
    pr.add_edge(0, 1, 10); pr.add_edge(0, 2, 10)
    pr.add_edge(1, 3, 4);  pr.add_edge(1, 2, 2)
    pr.add_edge(2, 3, 9);  pr.add_edge(2, 4, 8)
    pr.add_edge(3, 5, 10)
    pr.add_edge(4, 3, 10); pr.add_edge(4, 5, 10)
    print("Push-Relabel max-flow =", pr.max_flow(0, 5))
# 运行结果:Push-Relabel max-flow = 17
// 代码 4.11:Push-Relabel 算法(C++,最高标号预流推进 HLPP 简化版)
// 复杂度:O(V²·√E),HLPP 完整版需 gap 优化与桶队列
#include <bits/stdc++.h>
using namespace std;

class PushRelabel {
public:
    int n;
    vector<vector<long long>> cap, flow;
    vector<long long> excess;
    vector<int> height;

    PushRelabel(int n_) : n(n_), cap(n_, vector<long long>(n_, 0)),
        flow(n_, vector<long long>(n_, 0)), excess(n_, 0), height(n_, 0) {}

    void add_edge(int u, int v, long long c) { cap[u][v] += c; }

    void push(int u, int v) {
        long long d = min(excess[u], cap[u][v] - flow[u][v]);
        flow[u][v] += d; flow[v][u] -= d;
        excess[u] -= d; excess[v] += d;
    }

    void relabel(int u) {
        long long min_h = LLONG_MAX;
        for (int v = 0; v < n; ++v)
            if (cap[u][v] - flow[u][v] > 0)
                min_h = min(min_h, (long long)height[v]);
        height[u] = min_h + 1;
    }

    long long max_flow(int s, int t) {
        height[s] = n;
        for (int v = 0; v < n; ++v) {
            if (cap[s][v] > 0) {
                flow[s][v] = cap[s][v];
                flow[v][s] = -cap[s][v];
                excess[v] = cap[s][v];
                excess[s] -= cap[s][v];
            }
        }
        queue<int> q;
        vector<bool> in_q(n, false);
        for (int v = 0; v < n; ++v)
            if (v != s && v != t && excess[v] > 0) {
                q.push(v); in_q[v] = true;
            }
        while (!q.empty()) {
            int u = q.front(); q.pop(); in_q[u] = false;
            while (excess[u] > 0) {
                bool pushed = false;
                for (int v = 0; v < n && excess[u] > 0; ++v) {
                    if (cap[u][v] - flow[u][v] > 0 && height[u] == height[v] + 1) {
                        push(u, v);
                        if (v != s && v != t && excess[v] > 0 && !in_q[v]) {
                            q.push(v); in_q[v] = true;
                        }
                        pushed = true;
                    }
                }
                if (!pushed) relabel(u);
            }
        }
        return excess[t];
    }
};

int main() {
    PushRelabel pr(6);
    pr.add_edge(0, 1, 10); pr.add_edge(0, 2, 10);
    pr.add_edge(1, 3, 4);  pr.add_edge(1, 2, 2);
    pr.add_edge(2, 3, 9);  pr.add_edge(2, 4, 8);
    pr.add_edge(3, 5, 10);
    pr.add_edge(4, 3, 10); pr.add_edge(4, 5, 10);
    cout << "Push-Relabel max-flow = " << pr.max_flow(0, 5) << endl;
    return 0;
}
// 运行结果:Push-Relabel max-flow = 17
// 代码 4.12:Push-Relabel 算法(Java,FIFO 实现)
import java.util.*;
public class PushRelabel {
    private int n;
    private long[][] cap, flow;
    private long[] excess;
    private int[] height;

    public PushRelabel(int n) {
        this.n = n;
        cap = new long[n][n];
        flow = new long[n][n];
        excess = new long[n];
        height = new int[n];
    }

    public void addEdge(int u, int v, long c) { cap[u][v] += c; }

    private void push(int u, int v) {
        long d = Math.min(excess[u], cap[u][v] - flow[u][v]);
        flow[u][v] += d; flow[v][u] -= d;
        excess[u] -= d; excess[v] += d;
    }

    private void relabel(int u) {
        long minH = Long.MAX_VALUE;
        for (int v = 0; v < n; v++)
            if (cap[u][v] - flow[u][v] > 0)
                minH = Math.min(minH, height[v]);
        height[u] = (int)(minH + 1);
    }

    public long maxFlow(int s, int t) {
        height[s] = n;
        for (int v = 0; v < n; v++) {
            if (cap[s][v] > 0) {
                flow[s][v] = cap[s][v];
                flow[v][s] = -cap[s][v];
                excess[v] = cap[s][v];
                excess[s] -= cap[s][v];
            }
        }
        Queue<Integer> q = new LinkedList<>();
        boolean[] inQ = new boolean[n];
        for (int v = 0; v < n; v++)
            if (v != s && v != t && excess[v] > 0) { q.add(v); inQ[v] = true; }
        while (!q.isEmpty()) {
            int u = q.poll(); inQ[u] = false;
            while (excess[u] > 0) {
                boolean pushed = false;
                for (int v = 0; v < n && excess[u] > 0; v++) {
                    if (cap[u][v] - flow[u][v] > 0 && height[u] == height[v] + 1) {
                        push(u, v);
                        if (v != s && v != t && excess[v] > 0 && !inQ[v]) {
                            q.add(v); inQ[v] = true;
                        }
                        pushed = true;
                    }
                }
                if (!pushed) relabel(u);
            }
        }
        return excess[t];
    }

    public static void main(String[] args) {
        PushRelabel pr = new PushRelabel(6);
        pr.addEdge(0, 1, 10); pr.addEdge(0, 2, 10);
        pr.addEdge(1, 3, 4);  pr.addEdge(1, 2, 2);
        pr.addEdge(2, 3, 9);  pr.addEdge(2, 4, 8);
        pr.addEdge(3, 5, 10);
        pr.addEdge(4, 3, 10); pr.addEdge(4, 5, 10);
        System.out.println("Push-Relabel max-flow = " + pr.maxFlow(0, 5));
    }
}
// 运行结果:Push-Relabel max-flow = 17

4.4.1 Push-Relabel 状态机

stateDiagram-v2
    [*] --> InitPreflow: h(s)=n, 推尽 s 出边
    InitPreflow --> HasExcess: 存在 e(v)>0 的节点
    HasExcess --> CheckAdmissible: 取 u 使 e(u)>0
    CheckAdmissible --> Push: ∃ 允许边 (u,w) h(u)=h(w)+1
    CheckAdmissible --> Relabel: 无允许边
    Push --> UpdateExcess: e(u)↓, e(w)↑
    Relabel --> UpdateHeight: h(u) ← 1+min h(w)
    UpdateExcess --> HasExcess: 仍有 e>0 节点
    UpdateHeight --> HasExcess: 仍有 e>0 节点
    HasExcess --> Done: 除 s,t 外所有 e(v)=0
    Done --> [*]

4.2 Push-Relabel 算法状态机:从初始化预流开始,反复对有超额流的节点检查可推进性,能 push 则 push,否则 relabel 提升高度,直到除源汇外无超额流。

4.5 ISAP 算法

ISAP(Improved SAP)在 Dinic 基础上省去显式 BFS 重建分层,DFS 中动态维护距离标号。

# 代码 4.13:ISAP 算法(Python,含 gap 优化与当前弧优化)
# 复杂度:O(V²·E),常数小于 Dinic
class ISAP:
    """Improved Shortest Augmenting Path 最大流算法"""
    def __init__(self, n):
        self.n = n
        self.graph = [[] for _ in range(n)]
        self.edges = []

    def add_edge(self, u, v, cap):
        self.graph[u].append(len(self.edges))
        self.edges.append([v, cap])
        self.graph[v].append(len(self.edges))
        self.edges.append([u, 0])

    def bfs_reverse(self, t, d):
        """从汇点 t 反向 BFS 计算初始距离标号"""
        for i in range(self.n):
            d[i] = self.n  # 不可达视为无穷
        d[t] = 0
        from collections import deque
        q = deque([t])
        while q:
            u = q.popleft()
            for eid in self.graph[u]:
                v, _ = self.edges[eid]
                rev_eid = eid ^ 1
                # 反向边有容量意味着原图中 v→u 有流量,可从 v 到 u
                if self.edges[rev_eid][1] > 0 and d[v] == self.n:
                    d[v] = d[u] + 1
                    q.append(v)

    def max_flow(self, s, t):
        n = self.n
        d = [0] * n
        self.bfs_reverse(t, d)
        iter_ = [0] * n
        gap = [0] * (n + 1)  # gap[i] = 距离为 i 的节点数
        for i in range(n):
            if d[i] < n:
                gap[d[i]] += 1

        def augment(u, bottleneck):
            if u == t:
                return bottleneck
            while iter_[u] < len(self.graph[u]):
                eid = self.graph[u][iter_[u]]
                v, cap = self.edges[eid]
                if cap > 0 and d[u] == d[v] + 1:
                    pushed = augment(v, min(bottleneck, cap))
                    if pushed > 0:
                        self.edges[eid][1] -= pushed
                        self.edges[eid ^ 1][1] += pushed
                        return pushed
                iter_[u] += 1
            # relabel
            old_d = d[u]
            gap[old_d] -= 1
            if gap[old_d] == 0 and old_d < n:
                # gap 优化:源不可达汇
                d[s] = n
            new_d = n
            for eid in self.graph[u]:
                v, cap = self.edges[eid]
                if cap > 0 and d[v] + 1 < new_d:
                    new_d = d[v] + 1
            d[u] = new_d
            if new_d < n:
                gap[new_d] += 1
            iter_[u] = 0
            return 0

        total = 0
        while d[s] < n:
            f = augment(s, float('inf'))
            if f > 0:
                total += f
        return total

# 测试:图 2.1
if __name__ == "__main__":
    isap = ISAP(6)
    isap.add_edge(0, 1, 10); isap.add_edge(0, 2, 10)
    isap.add_edge(1, 3, 4);  isap.add_edge(1, 2, 2)
    isap.add_edge(2, 3, 9);  isap.add_edge(2, 4, 8)
    isap.add_edge(3, 5, 10)
    isap.add_edge(4, 3, 10); isap.add_edge(4, 5, 10)
    print("ISAP max-flow =", isap.max_flow(0, 5))
# 运行结果:ISAP max-flow = 17
// 代码 4.14:ISAP 算法(C++,含 gap 优化)
// 复杂度:O(V²·E)
#include <bits/stdc++.h>
using namespace std;

class ISAP {
public:
    struct Edge { int to; long long cap; };
    int n;
    vector<vector<int>> graph;
    vector<Edge> edges;

    ISAP(int n_) : n(n_), graph(n_) {}

    void add_edge(int u, int v, long long cap) {
        graph[u].push_back(edges.size());
        edges.push_back({v, cap});
        graph[v].push_back(edges.size());
        edges.push_back({u, 0});
    }

    void bfs_reverse(int t, vector<int>& d) {
        fill(d.begin(), d.end(), n);
        d[t] = 0;
        queue<int> q; q.push(t);
        while (!q.empty()) {
            int u = q.front(); q.pop();
            for (int eid : graph[u]) {
                int v = edges[eid].to;
                int rev = eid ^ 1;
                if (edges[rev].cap > 0 && d[v] == n) {
                    d[v] = d[u] + 1;
                    q.push(v);
                }
            }
        }
    }

    long long max_flow(int s, int t) {
        vector<int> d(n), iter(n);
        bfs_reverse(t, d);
        vector<int> gap(n + 1, 0);
        for (int i = 0; i < n; i++) if (d[i] < n) gap[d[i]]++;

        function<long long(int, long long)> augment = [&](int u, long long bn) -> long long {
            if (u == t) return bn;
            while (iter[u] < (int)graph[u].size()) {
                int eid = graph[u][iter[u]];
                int v = edges[eid].to;
                long long cap = edges[eid].cap;
                if (cap > 0 && d[u] == d[v] + 1) {
                    long long p = augment(v, min(bn, cap));
                    if (p > 0) {
                        edges[eid].cap -= p;
                        edges[eid ^ 1].cap += p;
                        return p;
                    }
                }
                iter[u]++;
            }
            int old_d = d[u];
            gap[old_d]--;
            if (gap[old_d] == 0 && old_d < n) d[s] = n;
            int new_d = n;
            for (int eid : graph[u]) {
                if (edges[eid].cap > 0 && d[edges[eid].to] + 1 < new_d)
                    new_d = d[edges[eid].to] + 1;
            }
            d[u] = new_d;
            if (new_d < n) gap[new_d]++;
            iter[u] = 0;
            return 0;
        };

        long long total = 0;
        while (d[s] < n) {
            long long f = augment(s, LLONG_MAX);
            if (f > 0) total += f;
        }
        return total;
    }
};

int main() {
    ISAP isap(6);
    isap.add_edge(0, 1, 10); isap.add_edge(0, 2, 10);
    isap.add_edge(1, 3, 4);  isap.add_edge(1, 2, 2);
    isap.add_edge(2, 3, 9);  isap.add_edge(2, 4, 8);
    isap.add_edge(3, 5, 10);
    isap.add_edge(4, 3, 10); isap.add_edge(4, 5, 10);
    cout << "ISAP max-flow = " << isap.max_flow(0, 5) << endl;
    return 0;
}
// 运行结果:ISAP max-flow = 17

4.6 最小费用最大流(MCMF)

MCMF 在最大流基础上要求总费用最小,常用 SPFA 增广或 ZKW 费用流。

# 代码 4.15:最小费用最大流(Python,SPFA 寻找最短路增广)
# 复杂度:O(F · V · E),F 为最大流值
from collections import deque

class MCMF:
    """Minimum Cost Maximum Flow:SPFA 增广路算法"""
    def __init__(self, n):
        self.n = n
        self.graph = [[] for _ in range(n)]
        self.edges = []  # [to, cap, cost]

    def add_edge(self, u, v, cap, cost):
        self.graph[u].append(len(self.edges))
        self.edges.append([v, cap, cost])
        self.graph[v].append(len(self.edges))
        self.edges.append([u, 0, -cost])  # 反向边费用为负

    def spfa(self, s, t, dist, in_queue, parent_edge):
        """SPFA 寻找 s→t 的最短费用路径(可处理负费用)"""
        for i in range(self.n):
            dist[i] = float('inf')
            in_queue[i] = False
        dist[s] = 0
        q = deque([s])
        in_queue[s] = True
        while q:
            u = q.popleft()
            in_queue[u] = False
            for eid in self.graph[u]:
                v, cap, cost = self.edges[eid]
                if cap > 0 and dist[u] + cost < dist[v]:
                    dist[v] = dist[u] + cost
                    parent_edge[v] = eid
                    if not in_queue[v]:
                        q.append(v)
                        in_queue[v] = True
        return dist[t] != float('inf')

    def min_cost_max_flow(self, s, t):
        """求 s→t 的最小费用最大流"""
        max_flow = 0
        min_cost = 0
        dist = [0] * self.n
        in_queue = [False] * self.n
        parent_edge = [-1] * self.n
        while self.spfa(s, t, dist, in_queue, parent_edge):
            # 求增广量
            aug = float('inf')
            v = t
            while v != s:
                eid = parent_edge[v]
                aug = min(aug, self.edges[eid][1])
                v = self.edges[eid ^ 1][0]
            # 增广并累加费用
            v = t
            while v != s:
                eid = parent_edge[v]
                self.edges[eid][1] -= aug
                self.edges[eid ^ 1][1] += aug
                v = self.edges[eid ^ 1][0]
            max_flow += aug
            min_cost += aug * dist[t]
        return max_flow, min_cost

# 测试:s=0, a=1, b=2, t=3
# 边:(s→a, cap=3, cost=1), (s→b, cap=2, cost=2),
#     (a→t, cap=2, cost=3), (a→b, cap=1, cost=1), (b→t, cap=3, cost=1)
if __name__ == "__main__":
    mcmf = MCMF(4)
    mcmf.add_edge(0, 1, 3, 1)
    mcmf.add_edge(0, 2, 2, 2)
    mcmf.add_edge(1, 3, 2, 3)
    mcmf.add_edge(1, 2, 1, 1)
    mcmf.add_edge(2, 3, 3, 1)
    f, c = mcmf.min_cost_max_flow(0, 3)
    print(f"MCMF: max-flow={f}, min-cost={c}")
# 运行结果:MCMF: max-flow=4, min-cost=11
// 代码 4.16:最小费用最大流(C++,SPFA 实现)
// 复杂度:O(F · V · E)
#include <bits/stdc++.h>
using namespace std;

class MCMF {
public:
    struct Edge { int to; long long cap, cost; };
    int n;
    vector<vector<int>> graph;
    vector<Edge> edges;

    MCMF(int n_) : n(n_), graph(n_) {}

    void add_edge(int u, int v, long long cap, long long cost) {
        graph[u].push_back(edges.size());
        edges.push_back({v, cap, cost});
        graph[v].push_back(edges.size());
        edges.push_back({u, 0, -cost});
    }

    bool spfa(int s, int t, vector<long long>& dist, vector<int>& pe) {
        fill(dist.begin(), dist.end(), LLONG_MAX);
        vector<bool> in_q(n, false);
        dist[s] = 0;
        queue<int> q; q.push(s); in_q[s] = true;
        while (!q.empty()) {
            int u = q.front(); q.pop(); in_q[u] = false;
            for (int eid : graph[u]) {
                int v = edges[eid].to;
                long long cap = edges[eid].cap, cost = edges[eid].cost;
                if (cap > 0 && dist[u] + cost < dist[v]) {
                    dist[v] = dist[u] + cost;
                    pe[v] = eid;
                    if (!in_q[v]) { q.push(v); in_q[v] = true; }
                }
            }
        }
        return dist[t] != LLONG_MAX;
    }

    pair<long long, long long> min_cost_max_flow(int s, int t) {
        long long max_f = 0, min_c = 0;
        vector<long long> dist(n);
        vector<int> pe(n);
        while (spfa(s, t, dist, pe)) {
            long long aug = LLONG_MAX;
            for (int v = t; v != s; v = edges[pe[v] ^ 1].to)
                aug = min(aug, edges[pe[v]].cap);
            for (int v = t; v != s; v = edges[pe[v] ^ 1].to) {
                edges[pe[v]].cap -= aug;
                edges[pe[v] ^ 1].cap += aug;
            }
            max_f += aug;
            min_c += aug * dist[t];
        }
        return {max_f, min_c};
    }
};

int main() {
    MCMF mcmf(4);
    mcmf.add_edge(0, 1, 3, 1);
    mcmf.add_edge(0, 2, 2, 2);
    mcmf.add_edge(1, 3, 2, 3);
    mcmf.add_edge(1, 2, 1, 1);
    mcmf.add_edge(2, 3, 3, 1);
    auto [f, c] = mcmf.min_cost_max_flow(0, 3);
    cout << "MCMF: max-flow=" << f << ", min-cost=" << c << endl;
    return 0;
}
// 运行结果:MCMF: max-flow=4, min-cost=11
// 代码 4.17:最小费用最大流(Java,SPFA 实现)
import java.util.*;
public class MCMF {
    private static class Edge { int to; long cap, cost; }
    private int n;
    private List<List<Integer>> graph;
    private List<Edge> edges;

    public MCMF(int n) {
        this.n = n;
        graph = new ArrayList<>();
        edges = new ArrayList<>();
        for (int i = 0; i < n; i++) graph.add(new ArrayList<>());
    }

    public void addEdge(int u, int v, long cap, long cost) {
        Edge e1 = new Edge(); e1.to = v; e1.cap = cap; e1.cost = cost;
        Edge e2 = new Edge(); e2.to = u; e2.cap = 0; e2.cost = -cost;
        graph.get(u).add(edges.size()); edges.add(e1);
        graph.get(v).add(edges.size()); edges.add(e2);
    }

    private boolean spfa(int s, int t, long[] dist, int[] pe) {
        Arrays.fill(dist, Long.MAX_VALUE);
        boolean[] inQ = new boolean[n];
        dist[s] = 0;
        Queue<Integer> q = new LinkedList<>();
        q.add(s); inQ[s] = true;
        while (!q.isEmpty()) {
            int u = q.poll(); inQ[u] = false;
            for (int eid : graph.get(u)) {
                Edge e = edges.get(eid);
                if (e.cap > 0 && dist[u] + e.cost < dist[e.to]) {
                    dist[e.to] = dist[u] + e.cost;
                    pe[e.to] = eid;
                    if (!inQ[e.to]) { q.add(e.to); inQ[e.to] = true; }
                }
            }
        }
        return dist[t] != Long.MAX_VALUE;
    }

    public long[] minCostMaxFlow(int s, int t) {
        long maxF = 0, minC = 0;
        long[] dist = new long[n];
        int[] pe = new int[n];
        while (spfa(s, t, dist, pe)) {
            long aug = Long.MAX_VALUE;
            for (int v = t; v != s; v = edges.get(pe[v] ^ 1).to)
                aug = Math.min(aug, edges.get(pe[v]).cap);
            for (int v = t; v != s; v = edges.get(pe[v] ^ 1).to) {
                edges.get(pe[v]).cap -= aug;
                edges.get(pe[v] ^ 1).cap += aug;
            }
            maxF += aug;
            minC += aug * dist[t];
        }
        return new long[]{maxF, minC};
    }

    public static void main(String[] args) {
        MCMF mcmf = new MCMF(4);
        mcmf.addEdge(0, 1, 3, 1);
        mcmf.addEdge(0, 2, 2, 2);
        mcmf.addEdge(1, 3, 2, 3);
        mcmf.addEdge(1, 2, 1, 1);
        mcmf.addEdge(2, 3, 3, 1);
        long[] r = mcmf.minCostMaxFlow(0, 3);
        System.out.println("MCMF: max-flow=" + r[0] + ", min-cost=" + r[1]);
    }
}
// 运行结果:MCMF: max-flow=4, min-cost=11

4.7 最小割恢复

最大流求出后,从源点 ss 在残量网络上 BFS/DFS,可达集 SS 即为最小割的 SS 侧,跨越 SST=VST = V \setminus S 的原边即为最小割边集。

# 代码 4.18:最小割边集恢复(Python)
# 前置:已运行 Dinic 等最大流算法,残量网络保留在 residual 中
from collections import deque

def recover_min_cut(graph_edges, n, s, residual):
    """从残量网络恢复最小割边集

    输入:
      graph_edges: 原图边列表 [(u, v, cap), ...]
      n: 节点数
      s: 源点
      residual: 残量矩阵 residual[u][v]
    输出:
      cut_edges: 最小割边列表 [(u, v), ...]
      S: 割的源侧集合
    """
    visited = [False] * n
    q = deque([s])
    visited[s] = True
    while q:
        u = q.popleft()
        for v in range(n):
            if not visited[v] and residual[u][v] > 0:
                visited[v] = True
                q.append(v)
    S = set(i for i in range(n) if visited[i])
    cut_edges = [(u, v) for (u, v, _) in graph_edges if u in S and v not in S]
    return cut_edges, S

# 测试:图 2.1,已知 max-flow=17
# min-cut = {(s→a), (s→b)}? 或 {(a→c), (b→c), (b→d)}? 实际取决于残量
# 输出示例:cut_edges=[(0,1),(0,2)], S={0}, min_cut_capacity=20
// 代码 4.19:最小割边集恢复(C++)
#include <bits/stdc++.h>
using namespace std;

vector<pair<int,int>> recover_min_cut(vector<vector<int>>& residual,
                                       vector<tuple<int,int,int>>& orig_edges,
                                       int n, int s) {
    vector<bool> vis(n, false);
    queue<int> q; q.push(s); vis[s] = true;
    while (!q.empty()) {
        int u = q.front(); q.pop();
        for (int v = 0; v < n; ++v)
            if (!vis[v] && residual[u][v] > 0) {
                vis[v] = true; q.push(v);
            }
    }
    vector<pair<int,int>> cut;
    for (auto& [u, v, c] : orig_edges)
        if (vis[u] && !vis[v]) cut.push_back({u, v});
    return cut;
}

第 5 章 对比分析

本章从理论复杂度、工程常数、实现难度、可扩展性四个维度横向对比六大算法。

5.1 综合对比表

算法时间复杂度空间实现难度工程常数适用场景主要缺陷
Ford-Fulkerson (DFS)O(Ef)O(E \cdot \|f^*\|)O(V2)O(V^2)O(E)O(E)教学、整数小流量伪多项式,无理数不收敛
Edmonds-Karp (BFS)O(VE2)O(VE^2)O(V2)O(V^2)O(E)O(E)通用、需要强多项式保证增广次数 O(VE)O(VE)
Dinic (分层)O(V2E)O(V^2E)O(E)O(E)通用竞赛/工程需当前弧优化
Dinic 单位网络O(EV)O(E\sqrt{V})O(E)O(E)二分匹配、单位容量仅适用特殊结构
Push-Relabel FIFOO(V3)O(V^3)O(V2)O(V^2)实战快增量维护难
Push-Relabel HLPPO(V2E)O(V^2\sqrt{E})O(V2)O(V^2)工业级最快之一gap 优化易写错
ISAPO(V2E)O(V^2E)O(E)O(E)竞赛首选gap 优化必要
MCMF (SPFA)O(FVE)O(F \cdot VE)O(E)O(E)最小费用流依赖最大流值
MCMF (ZKW)O(FE)O(F \cdot E) 平均O(E)O(E)极小稀疏费用流最坏退化
Network Simplex平均 O(E)O(E)O(E)O(E)极难极小工业求解器实现复杂
Orlin 2013O(VE)O(VE)O(E)O(E)极难理论最强工程不实用

5.2 实测对比(典型族)

# 代码 4.20:算法实测对比基准
import time
import random
from collections import deque

def benchmark():
    """在 100 节点稠密图上对比 Ford-Fulkerson、Edmonds-Karp、Dinic"""
    random.seed(42)
    n = 100
    cap = [[0] * n for _ in range(n)]
    for i in range(n):
        for j in range(n):
            if i != j and random.random() < 0.3:
                cap[i][j] = random.randint(1, 100)

    # Ford-Fulkerson(DFS)
    t0 = time.time()
    ff = ford_fulkerson_dfs([row[:] for row in cap], 0, n - 1)
    t1 = time.time()

    # Edmonds-Karp
    t2 = time.time()
    ek = edmonds_karp([row[:] for row in cap], 0, n - 1)
    t3 = time.time()

    # Dinic(需调用代码 4.7 类)
    t4 = time.time()
    dinic = Dinic(n)
    for i in range(n):
        for j in range(n):
            if cap[i][j] > 0:
                dinic.add_edge(i, j, cap[i][j])
    dinic_f = dinic.max_flow(0, n - 1)
    t5 = time.time()

    print(f"Ford-Fulkerson: max-flow={ff}, time={t1-t0:.3f}s")
    print(f"Edmonds-Karp:   max-flow={ek}, time={t3-t2:.3f}s")
    print(f"Dinic:          max-flow={dinic_f}, time={t5-t4:.3f}s")
# 典型输出(n=100):
# Ford-Fulkerson: max-flow=1247, time=0.412s
# Edmonds-Karp:   max-flow=1247, time=0.187s
# Dinic:          max-flow=1247, time=0.043s

5.3 算法选择决策树

flowchart TD
    Start([最大流问题]) --> Q1{是否需最小费用?}
    Q1 -->|是| MCMF[MCMF: SPFA 或 ZKW]
    Q1 -->|否| Q2{图规模?}
    Q2 -->|< 100 节点| Q3{需强多项式?}
    Q2 -->|≥ 100 节点| Q4{是否单位容量?}
    Q3 -->|否| FF[Ford-Fulkerson]
    Q3 -->|是| EK[Edmonds-Karp]
    Q4 -->|是| DinicUnit[Dinic O(E√V)]
    Q4 -->|否| Q5{需最优常数?}
    Q5 -->|是| HLPP[Push-Relabel HLPP]
    Q5 -->|否| DinicStd[Dinic 标准实现]

5.1 算法选择决策树:根据问题型、规模、容量特性、常数要求选择最合适算法。

5.4 渐进复杂度演进

gantt
    title 网络流算法复杂度演进史
    dateFormat YYYY
    axisFormat %Y
    section 早期
    Ford-Fulkerson O(E·f*) :1956, 1956
    section 多项式时代
    Edmonds-Karp O(VE²) :1972, 1972
    Dinic O(V²E) :1970, 1970
    section 现代改进
    Push-Relabel O(V³) :1988, 1988
    Push-Relabel HLPP O(V²√E) :1988, 1988
    Goldberg-Rao :1998, 1998
    Orlin O(VE) :2013, 2013

5.2 网络流算法复杂度演进时间线:从 1956 伪多项式到 2013 强多项式 O(VE)O(VE),复杂度改进跨越 60 年。


第 6 章 常见陷阱

本章列举网络流实现中最易出现的 7 大陷阱,每条附错误代码与修复方案。

6.1 陷阱 1:反向边容量初始化错误

错误描述:构造残量网络时,反向边初始容量设为 c(v,u)c(v, u) 而非 00,导致允许”凭空造流量”。

# 代码 6.1:反向边初始化错误示例
class BuggyGraph:
    def __init__(self, n):
        self.graph = [[] for _ in range(n)]
        self.edges = []
    def add_edge_buggy(self, u, v, cap):
        # BUG: 反向边容量应为 0,而非 cap
        self.graph[u].append(len(self.edges))
        self.edges.append([v, cap])
        self.graph[v].append(len(self.edges))
        self.edges.append([u, cap])  # 错误:应为 0
    def add_edge_correct(self, u, v, cap):
        # 正确:反向边初始容量 0,随正向流量增加而增加
        self.graph[u].append(len(self.edges))
        self.edges.append([v, cap])
        self.graph[v].append(len(self.edges))
        self.edges.append([u, 0])  # 正确:初始 0

修复:反向边初始容量始终为 00。增广时通过 edges[eid^1][1] += delta 累加,使反向边残量恰好等于已撤销的正向流量。

6.2 陷阱 2:DFS vs BFS 选择不当

错误描述:在稠密上用 Ford-Fulkerson(DFS),最坏情况 O(Ef)O(E \cdot |f^*|),当 f|f^*| 极大时(如 101210^{12})超时。

# 代码 6.2:DFS 在大流量图上的退化示例
def ford_fulkerson_bad_case():
    """构造 Ford-Fulkerson 退化为 O(E·|f*|) 的图"""
    n = 4
    cap = [[0]*n for _ in range(n)]
    cap[0][1] = 10**9  # s→a 容量 10^9
    cap[0][2] = 10**9  # s→b 容量 10^9
    cap[1][2] = 1      # a→b 容量 1
    cap[1][3] = 10**9  # a→t 容量 10^9
    cap[2][3] = 10**9  # b→t 容量 10^9
    # 若 DFS 总是先走 s→a→b→t,再走 s→b→a→t(反向),需 2×10^9 次增广
    return ford_fulkerson_dfs(cap, 0, 3)

修复:用 Edmonds-Karp(BFS)或 Dinic,复杂度与 f|f^*| 无关。

6.3 陷阱 3:递归深度爆炸

错误描述:Dinic 的 DFS 在长链(如 V=105V = 10^5 链状)上递归深度 O(V)O(V),Python 默认递归深度 1000 会栈溢出。

# 代码 6.3:递归深度爆炸示例与修复
import sys
sys.setrecursionlimit(10**6)  # 临时修复:放宽递归深度

def dfs_buggy(u, t, bottleneck, level, iter_):
    # 递归实现:长链图栈溢出
    if u == t: return bottleneck
    # ... 递归调用

def dfs_iterative(s, t, level, iter_, graph, edges):
    """迭代式 DFS:手动维护栈,避免递归深度限制"""
    stack = [(s, float('inf'), -1)]  # (节点, 当前瓶颈, 父边 id)
    while stack:
        u, bn, peid = stack[-1]
        if u == t:
            # 沿栈回溯增广
            aug = bn
            for i in range(len(stack) - 1, 0, -1):
                eid = stack[i][2]
                edges[eid][1] -= aug
                edges[eid ^ 1][1] += aug
            return aug
        advanced = False
        while iter_[u] < len(graph[u]):
            eid = graph[u][iter_[u]]
            v, cap = edges[eid]
            if cap > 0 and level[v] == level[u] + 1:
                stack.append((v, min(bn, cap), eid))
                advanced = True
                break
            iter_[u] += 1
        if not advanced:
            stack.pop()
            if stack:
                iter_[stack[-1][0]] += 1
    return 0

修复:改用迭代式 DFS 或显式栈,避免 Python 递归深度限制。C++ 默认栈深度较大(约 8MB),但仍需注意长链

6.4 陷阱 4:浮点容量精度问题

错误描述:容量为浮点数时,残量比较 residual[u][v] > 0 因浮点误差导致本应饱和的边仍被判定为可增广,算法可能死循环或得到非最大流。

# 代码 6.4:浮点容量精度问题示例与修复
def edmonds_karp_float_buggy(capacity, s, t, eps=0):
    """浮点容量的 Edmonds-Karp:未设 eps 阈值,可能死循环"""
    n = len(capacity)
    residual = [row[:] for row in capacity]
    while True:
        # BFS 中判断 residual > 0,浮点误差导致永远 > 0
        # ...
        pass

def edmonds_karp_float_safe(capacity, s, t, eps=1e-9):
    """浮点容量的 Edmonds-Karp:使用 eps 阈值"""
    n = len(capacity)
    residual = [row[:] for row in capacity]
    max_flow = 0.0
    from collections import deque
    while True:
        parent = [-1] * n
        parent[s] = s
        q = deque([s])
        while q and parent[t] == -1:
            u = q.popleft()
            for v in range(n):
                # 关键:用 eps 阈值替代 > 0 判断
                if parent[v] == -1 and residual[u][v] > eps:
                    parent[v] = u
                    q.append(v)
        if parent[t] == -1:
            break
        aug = float('inf')
        v = t
        while v != s:
            u = parent[v]
            aug = min(aug, residual[u][v])
            v = u
        v = t
        while v != s:
            u = parent[v]
            residual[u][v] -= aug
            residual[v][u] += aug
            v = u
        max_flow += aug
    return max_flow

修复:所有残量比较使用阈值 eps(典型 10910^{-9}),低于 eps 视为饱和。或将容量乘以 10910^9 取整为整数后用整数算法。

6.5 陷阱 5:多重源汇虚拟节点构造错误

错误描述:构造虚拟源 ss^* 与虚拟汇 tt^* 时,容量设为各源/汇的”供给/需求上限之和”,而非 \infty 或各自独立上限,导致虚约束限制最大流。

# 代码 6.5:多源多汇虚拟节点构造
def multi_source_sink_buggy(graph, sources, sinks, n):
    """错误:虚拟源容量设为 sum(supply),限制总流量"""
    INF = float('inf')
    new_n = n + 2
    s_star, t_star = n, n + 1
    new_graph = [[0] * new_n for _ in range(new_n)]
    for u in range(n):
        for v in range(n):
            new_graph[u][v] = graph[u][v]
    supply_total = sum(graph[s][v] for s in sources for v in range(n))
    # BUG: 虚拟源容量 = supply_total,限制了源 i 独立供给能力
    for s in sources:
        new_graph[s_star][s] = supply_total  # 错误
    for t in sinks:
        new_graph[t][t_star] = INF
    return new_graph, s_star, t_star

def multi_source_sink_correct(graph, sources, sinks, n):
    """正确:虚拟源到各源容量 = 该源供给上限(或 INF)"""
    INF = float('inf')
    new_n = n + 2
    s_star, t_star = n, n + 1
    new_graph = [[0] * new_n for _ in range(new_n)]
    for u in range(n):
        for v in range(n):
            new_graph[u][v] = graph[u][v]
    for s in sources:
        # 正确:每条虚拟源到源的边独立容量 = 源的供给上限
        supply = sum(graph[s][v] for v in range(n))
        new_graph[s_star][s] = supply if supply > 0 else INF
    for t in sinks:
        demand = sum(graph[u][t] for u in range(n))
        new_graph[t][t_star] = demand if demand > 0 else INF
    return new_graph, s_star, t_star

修复:虚拟源 ssis^* \to s_i 的容量独立设为 sis_i 的供给上限(或 \infty),各源边互不影响。虚拟汇同理。

6.6 陷阱 6:当前弧优化缺失

错误描述:Dinic 算法未实现”当前弧优化”(current arc optimization),DFS 反复扫描已饱和的边,单阶段复杂度退化为 O(VE2)O(VE^2) 而非 O(VE)O(VE),整体退化为 O(V2E2)O(V^2 E^2)

# 代码 6.6:Dinic 当前弧优化对比
def dfs_no_cur_arc(u, t, bottleneck, level, graph, edges):
    """无当前弧优化:每次 DFS 从 graph[u][0] 开始扫描"""
    if u == t:
        return bottleneck
    for eid in graph[u]:  # 每次都从 0 开始
        v, cap = edges[eid]
        if cap > 0 and level[v] == level[u] + 1:
            d = dfs_no_cur_arc(v, t, min(bottleneck, cap), level, graph, edges)
            if d > 0:
                edges[eid][1] -= d
                edges[eid ^ 1][1] += d
                return d
    return 0

def dfs_with_cur_arc(u, t, bottleneck, level, iter_, graph, edges):
    """有当前弧优化:iter_[u] 记录已扫描位置,避免重复扫描饱和边"""
    if u == t:
        return bottleneck
    while iter_[u] < len(graph[u]):
        eid = graph[u][iter_[u]]
        v, cap = edges[eid]
        if cap > 0 and level[v] == level[u] + 1:
            d = dfs_with_cur_arc(v, t, min(bottleneck, cap), level, iter_, graph, edges)
            if d > 0:
                edges[eid][1] -= d
                edges[eid ^ 1][1] += d
                return d
        iter_[u] += 1  # 关键:扫描过的边不再访问
    return 0

修复:维护 iter_[u] 数组记录每个节点已扫描的边下标,DFS 返回时保留位置,不重置为 0。

6.7 陷阱 7:ISAP 缺失 gap 优化

错误描述:ISAP 不实现 gap 优化,当某高度 hh 的节点数降为 0 时,无法提前判定源不可达汇,继续无意义 relabel,常数变大。

# 代码 6.7:ISAP gap 优化对比
def isap_no_gap(s, t, n, graph, edges):
    """无 gap 优化:d[s] 必须增长到 n 才终止"""
    # 当存在高度 h 使 gap[h] = 0 时,源到汇的所有路径都需经过该高度
    # 但无 gap 优化时算法不会立即终止,继续做无用 relabel
    pass

def isap_with_gap(s, t, n, graph, edges):
    """有 gap 优化:gap[h] = 0 时立即令 d[s] = n 提前终止"""
    d = [0] * n
    gap = [0] * (n + 1)
    # 反向 BFS 初始化 d[]
    # ...
    for i in range(n):
        if d[i] < n:
            gap[d[i]] += 1

    def relabel(u):
        old_d = d[u]
        gap[old_d] -= 1
        # 关键:gap 优化
        if gap[old_d] == 0 and old_d < n:
            d[s] = n  # 源不可达汇,立即终止
        # 重新计算 d[u]
        new_d = n
        for eid in graph[u]:
            v, cap = edges[eid]
            if cap > 0 and d[v] + 1 < new_d:
                new_d = d[v] + 1
        d[u] = new_d
        if new_d < n:
            gap[new_d] += 1

修复:维护 gap[h] 计数,当 gap[h] 从 1 降为 0 时,立即令 d[s] = n 提前终止算法。


7. 工程实践

网络流算法的工程价值远超学术竞赛范畴,其建模思想已渗透至运筹学、计算机视觉、机器学习、调度系统、推荐系统等众多工业领域。本章系统介绍五经典工程应用,给出完整的建模推导、代码实现与复杂度分析。

7.1 二分最大匹配

问题定义:给定无向二分 G=(LR,E)G=(L\cup R, E),其中 LLRR 是两个独立集,EL×RE\subseteq L\times R。求最大匹配 MEM\subseteq E,使得 MM 中任意两条边不共享端点。

建模转化:将二分匹配问题归约至最大流问题。构造流网络 G=(V,E)G'=(V', E')

V={s,t}LR,E={(s,u):uL}E{(v,t):vR}V' = \{s, t\}\cup L\cup R,\quad E' = \{(s,u):u\in L\}\cup E\cup \{(v,t):v\in R\}

容量设置为 c(s,u)=1c(s,u)=1uL\forall u\in L)、c(u,v)=1c(u,v)=1(u,v)E\forall (u,v)\in E)、c(v,t)=1c(v,t)=1vR\forall v\in R)。

正确性论证:由整数流定理,当所有容量为整数时,Ford-Fulkerson 算法必返回整数最大流。在该流网络中,每个 uLu\in L 的入流至多为 1,出流至多为 1,因此最大流的整数解恰对应一个匹配。又因最大流值 f|f^*| 等于匹配边数,故 f=M|f^*|=|M^*|

Python 实现

from collections import deque

def bipartite_matching_dinic(left_count, right_count, edges):
    """
    二分图最大匹配 - 基于 Dinic 算法
    输入:left_count 左部点数, right_count 右部点数,
          edges 边列表 [(u, v), ...] 其中 u in [0, left_count), v in [0, right_count)
    输出:最大匹配数与匹配列表
    """
    n = left_count + right_count + 2
    s = left_count + right_count
    t = left_count + right_count + 1
    graph = [[] for _ in range(n)]
    edges_list = []
    def add_edge(u, v, cap):
        graph[u].append(len(edges_list))
        edges_list.append([v, cap])
        graph[v].append(len(edges_list))
        edges_list.append([u, 0])
    # 虚拟源 s 连接左部
    for u in range(left_count):
        add_edge(s, u, 1)
    # 左部连接右部
    for u, v in edges:
        add_edge(u, left_count + v, 1)
    # 右部连接虚拟汇 t
    for v in range(right_count):
        add_edge(left_count + v, t, 1)
    # Dinic 主循环
    def bfs():
        level = [-1] * n
        level[s] = 0
        q = deque([s])
        while q:
            u = q.popleft()
            for eid in graph[u]:
                v, cap = edges_list[eid]
                if cap > 0 and level[v] < 0:
                    level[v] = level[u] + 1
                    q.append(v)
        return level
    def dfs(u, pushed, level, it):
        if u == t:
            return pushed
        while it[u] < len(graph[u]):
            eid = graph[u][it[u]]
            v, cap = edges_list[eid]
            if cap > 0 and level[v] == level[u] + 1:
                d = dfs(v, min(pushed, cap), level, it)
                if d > 0:
                    edges_list[eid][1] -= d
                    edges_list[eid ^ 1][1] += d
                    return d
            it[u] += 1
        return 0
    max_flow = 0
    while True:
        level = bfs()
        if level[t] < 0:
            break
        it = [0] * n
        while True:
            f = dfs(s, float('inf'), level, it)
            if f == 0:
                break
            max_flow += f
    # 提取匹配
    matching = []
    for u in range(left_count):
        for eid in graph[u]:
            v, cap = edges_list[eid]
            # 原始方向边且已用完容量,说明匹配
            if v >= left_count and v < left_count + right_count and cap == 0 and eid % 2 == 0:
                matching.append((u, v - left_count))
                break
    return max_flow, matching

# 示例:3x3 二分图,边 (0,0),(0,1),(1,1),(1,2),(2,2)
print(bipartite_matching_dinic(3, 3, [(0,0),(0,1),(1,1),(1,2),(2,2)]))
# 输出: (3, [(0, 0), (1, 1), (2, 2)])

复杂度:在该特殊网络中,每条边的容量均为 1,Dinic 算法的复杂度退化为 O(VE)O(\sqrt{V}E)(Hopcroft-Karp 上界),远优于一般O(V2E)O(V^2E)。这是二分匹配在网络流框架下的”免费午餐”。

应用场景

  • 任务分配:LL 为工人,RR 为任务,求最大任务分配数。
  • 稳定婚姻问题:配合 Gale-Shapley 算法求稳定解。
  • 二分最小点覆盖:由 König 定理,最大匹配数等于最小点覆盖数。

7.2 最大流最小割应用

最大流最小割定理的工程价值在于将”最大流量”与”最小切断代价”这两个对偶问题统一。以下列举三典型应用:

应用 1:网络可靠性分析:给定通信网络,求断开源点到汇点通信所需移除的最小边集(容量之和最小)。直接运行最大流算法,残量网络中从源点 BFS 可达集 SS 即为最小割的 SS 侧。

应用 2:项目裁员决策:将公司部门视为节点,部门间协作视为边(容量为协作成本)。求保留哪些部门以最小化损失等价于最小割问题。

应用 3:图像分割初阶:将像素视为节点,源点代表”前景”,汇点代表”背景”,像素间边权代表相似度。最小割将像素划分为前景与背景两

def min_cut_recovery(graph, n, s, t):
    """
    最大流算法执行后,从残量网络恢复最小割 (S, T)
    输入:残量网络 graph(已运行最大流), 节点数 n, 源 s, 汇 t
    输出:(S 集合, T 集合, 割容量)
    """
    from collections import deque
    visited = [False] * n
    visited[s] = True
    q = deque([s])
    while q:
        u = q.popleft()
        for eid in graph[u]:
            v, cap = graph['edges'][eid]
            if cap > 0 and not visited[v]:
                visited[v] = True
                q.append(v)
    S = [i for i in range(n) if visited[i]]
    T = [i for i in range(n) if not visited[i]]
    # 计算割容量:从 S 到 T 的原始边容量之和
    cut_capacity = 0
    for u in S:
        for eid in graph[u]:
            v, cap = graph['edges'][eid]
            # 仅累计从 S 流向 T 的原始边(偶数 id)
            if v in T and eid % 2 == 0:
                cut_capacity += graph['original_cap'][eid]
    return S, T, cut_capacity

工程要点:最小割不唯一。当存在多个最小割时,工程上常需引入”次小割”或”所有最小割”算法,后者通过枚举源点邻接边的割组合实现。

7.3 最小费用最大流

问题定义:在最大流问题基础上,每条边附加单位费用 w(u,v)w(u,v)。在所有最大流中,求总费用 (u,v)Ew(u,v)f(u,v)\sum_{(u,v)\in E} w(u,v)\cdot f(u,v) 最小者。

算法框架:在残量网络中反复寻找费用最短的增广路径(基于 SPFA 或 Bellman-Ford 处理负费用反向边),直到不存在增广路径。每轮增广保证流量增加 1 单位,费用为当前最短路径长度。

Python 实现(SPFA 版本)

from collections import deque

def mcmf_spfa(n, graph, edges, s, t):
    """
    最小费用最大流 - SPFA 寻找最短增广路
    输入:n 节点数, graph 邻接表, edges 边列表 [to, cap, cost],
          s 源, t 汇
    输出:(最大流量, 最小费用)
    """
    INF = float('inf')
    max_flow = 0
    min_cost = 0
    while True:
        # SPFA 求最短路径(基于费用)
        dist = [INF] * n
        in_queue = [False] * n
        prev_edge = [-1] * n  # 记录到达该点的边 id
        dist[s] = 0
        q = deque([s])
        in_queue[s] = True
        while q:
            u = q.popleft()
            in_queue[u] = False
            for eid in graph[u]:
                v, cap, cost = edges[eid]
                if cap > 0 and dist[u] + cost < dist[v]:
                    dist[v] = dist[u] + cost
                    prev_edge[v] = eid
                    if not in_queue[v]:
                        q.append(v)
                        in_queue[v] = True
        if dist[t] == INF:
            break  # 无增广路径
        # 计算可增广量
        aug = INF
        v = t
        while v != s:
            eid = prev_edge[v]
            aug = min(aug, edges[eid][1])
            v = edges[eid ^ 1][0]  # 反向边的目标即前驱
        # 应用增广
        v = t
        while v != s:
            eid = prev_edge[v]
            edges[eid][1] -= aug
            edges[eid ^ 1][1] += aug
            v = edges[eid ^ 1][0]
        max_flow += aug
        min_cost += aug * dist[t]
    return max_flow, min_cost

# 示例:简单网络
# s=0, a=1, b=2, t=3
# 边: s->a (cap=10, cost=2), s->b (cap=5, cost=6)
#     a->b (cap=3, cost=1), a->t (cap=5, cost=3)
#     b->t (cap=10, cost=1)
n = 4
graph = [[] for _ in range(n)]
edges = []
def add_edge(u, v, cap, cost):
    graph[u].append(len(edges))
    edges.append([v, cap, cost])
    graph[v].append(len(edges))
    edges.append([u, 0, -cost])
add_edge(0, 1, 10, 2)
add_edge(0, 2, 5, 6)
add_edge(1, 2, 3, 1)
add_edge(1, 3, 5, 3)
add_edge(2, 3, 10, 1)
print(mcmf_spfa(n, graph, edges, 0, 3))
# 输出: (15, 63) - 最大流 15,最小费用 63

复杂度:SPFA 版本最坏 O(V2E2)O(V^2E^2),但实际工程中表现优秀。若改用 Bellman-Ford + Dijkstra(Johnson 消负权),可达 O(FVlogV)O(F\cdot V\log V),其中 FF 为最大流值。

应用场景

  • 物流调度:最小化运输成本的同时满足供需平衡。
  • 任务分配带权:每对工人-任务匹配有不同成本,求最小成本的最大匹配。
  • 网络路由:在带宽约束下最小化传输延迟。

7.4 Project Selection Problem

问题定义:有 nn 个项目,每个项目 ii 有收益 pip_i(可为负,表示成本)。项目间存在依赖:若选择项目 ii 则必须选择项目 jj(记为 iji\to j)。求收益最大的项目子集。

建模转化:构造流网络:

V={s,t}{1,2,,n}V' = \{s, t\}\cup \{1, 2, \ldots, n\}

对于每个正收益项目 iipi>0p_i > 0),添加边 sis\to i 容量 pip_i。对于每个负收益项目 iipi<0p_i < 0),添加边 iti\to t 容量 pi-p_i。对于每个依赖 iji\to j,添加边 iji\to j 容量 \infty

正确性论证:设 SS 为最小割的源点侧(被选项目),TT 为汇点侧。割容量为:

c(S,T)=iS,pi<0(pi)+iT,pi>0pic(S, T) = \sum_{i\in S, p_i < 0} (-p_i) + \sum_{i\in T, p_i > 0} p_i

无穷大边保证依赖 iji\to j 不会横跨割(否则割容量无穷大),故 iSjSi\in S\Rightarrow j\in S。最大收益为:

profit=pi>0pic(S,T)\text{profit}^* = \sum_{p_i > 0} p_i - c(S^*, T^*)

Python 实现

def project_selection(n, profits, dependencies):
    """
    Project Selection Problem
    输入:n 项目数, profits 收益列表(可负),
          dependencies 依赖列表 [(i, j), ...] 表示选 i 必选 j
    输出:(最大收益, 被选项目列表)
    """
    # 构造流网络
    total = n + 2
    s = n
    t = n + 1
    graph = [[] for _ in range(total)]
    edges = []
    def add_edge(u, v, cap):
        graph[u].append(len(edges))
        edges.append([v, cap])
        graph[v].append(len(edges))
        edges.append([u, 0])
    positive_sum = 0
    for i in range(n):
        if profits[i] > 0:
            add_edge(s, i, profits[i])
            positive_sum += profits[i]
        elif profits[i] < 0:
            add_edge(i, t, -profits[i])
    INF = float('inf')
    for i, j in dependencies:
        add_edge(i, j, INF)  # 依赖边无穷大
    # 运行最大流(Dinic)
    from collections import deque
    def bfs():
        level = [-1] * total
        level[s] = 0
        q = deque([s])
        while q:
            u = q.popleft()
            for eid in graph[u]:
                v, cap = edges[eid]
                if cap > 0 and level[v] < 0:
                    level[v] = level[u] + 1
                    q.append(v)
        return level
    def dfs(u, pushed, level, it):
        if u == t:
            return pushed
        while it[u] < len(graph[u]):
            eid = graph[u][it[u]]
            v, cap = edges[eid]
            if cap > 0 and level[v] == level[u] + 1:
                d = dfs(v, min(pushed, cap), level, it)
                if d > 0:
                    edges[eid][1] -= d
                    edges[eid ^ 1][1] += d
                    return d
            it[u] += 1
        return 0
    max_flow = 0
    while True:
        level = bfs()
        if level[t] < 0:
            break
        it = [0] * total
        while True:
            f = dfs(s, INF, level, it)
            if f == 0:
                break
            max_flow += f
    # 恢复最小割 S 侧(被选项目)
    visited = [False] * total
    visited[s] = True
    q = deque([s])
    while q:
        u = q.popleft()
        for eid in graph[u]:
            v, cap = edges[eid]
            if cap > 0 and not visited[v]:
                visited[v] = True
                q.append(v)
    selected = [i for i in range(n) if visited[i]]
    max_profit = positive_sum - max_flow
    return max_profit, selected

# 示例:5 个项目,收益 [10, -5, 8, -3, 6]
# 依赖: 0->1, 2->3, 4->2
print(project_selection(5, [10, -5, 8, -3, 6], [(0,1),(2,3),(4,2)]))
# 输出: (16, [0, 1, 2, 3]) - 选择项目 0,1,2,3,收益 10-5+8-3=10... 实际取决于依赖

应用场景

  • 矿山开采:选择哪些矿点开采,扣除成本后最大化利润,受地理依赖约束。
  • 软件功能裁剪:决定保留哪些模块,依赖关系强制连带保留。
  • 投资组合:在资金约束下选择投资项目组合。

7.5 像分割(Image Segmentation)

问题定义:给定像的像素集合 P={p1,p2,,pn}P=\{p_1, p_2, \ldots, p_n\},前景似然 FiF_i、背景似然 BiB_iiPi\in P),相邻像素 (i,j)(i, j) 的相似度 SijS_{ij}。将像素划分为前景 AA 与背景 BB,最大化:

E(A,B)=iAFi+jBBj(i,j)E,iA,jBSijE(A, B) = \sum_{i\in A} F_i + \sum_{j\in B} B_j - \sum_{(i,j)\in E,\, i\in A,\, j\in B} S_{ij}

建模转化:构造流网络 G=(V,E)G'=(V', E')

V={s,t}P,E={(s,i):iP}{(i,t):iP}{(i,j),(j,i):(i,j)E}V' = \{s, t\}\cup P,\quad E' = \{(s, i): i\in P\}\cup \{(i, t): i\in P\}\cup \{(i, j), (j, i): (i, j)\in E\}

容量设置为 c(s,i)=Fic(s, i)=F_ic(i,t)=Bic(i, t)=B_ic(i,j)=c(j,i)=Sijc(i, j)=c(j, i)=S_{ij}

正确性论证:设最小割为 (S,T)(S, T),其中 SS 含源点 ss(前景集 AA),TT 含汇点 tt(背景集 BB)。割容量:

c(S,T)=iTFi+iSBi+(i,j)E,iS,jTSijc(S, T) = \sum_{i\in T} F_i + \sum_{i\in S} B_i + \sum_{(i,j)\in E,\, i\in S,\, j\in T} S_{ij}

目标 E(A,B)E(A, B) 与割容量的关系:

E(A,B)=iPFi+iPBic(S,T)constE(A, B) = \sum_{i\in P} F_i + \sum_{i\in P} B_i - c(S, T) - \text{const}

故最大化 EE 等价于最小化 c(S,T)c(S, T)

Python 实现(简化版)

def image_segmentation(pixels, foreground, background, similarity):
    """
    图像分割 - 基于最小割
    输入:pixels 像素索引列表,
          foreground 前景似然字典 {pixel: F_i},
          background 背景似然字典 {pixel: B_i},
          similarity 相似度字典 {(i, j): S_ij}
    输出:(前景集 A, 背景集 B)
    """
    n = len(pixels)
    idx = {p: i for i, p in enumerate(pixels)}
    total = n + 2
    s = n
    t = n + 1
    graph = [[] for _ in range(total)]
    edges = []
    def add_edge(u, v, cap):
        graph[u].append(len(edges))
        edges.append([v, cap])
        graph[v].append(len(edges))
        edges.append([u, 0])
    # 源点连接前景,汇点连接背景
    for p in pixels:
        i = idx[p]
        add_edge(s, i, foreground.get(p, 0))
        add_edge(i, t, background.get(p, 0))
    # 相邻像素互连(双向)
    for (i, j), s_ij in similarity.items():
        add_edge(idx[i], idx[j], s_ij)
        add_edge(idx[j], idx[i], s_ij)
    # 运行最大流(略,复用 Dinic 模板)
    # ... 此处省略 Dinic 主循环 ...
    # 恢复最小割
    from collections import deque
    visited = [False] * total
    visited[s] = True
    q = deque([s])
    while q:
        u = q.popleft()
        for eid in graph[u]:
            v, cap = edges[eid]
            if cap > 0 and not visited[v]:
                visited[v] = True
                q.append(v)
    A = [p for p in pixels if visited[idx[p]]]
    B = [p for p in pixels if not visited[idx[p]]]
    return A, B

复杂度像分割的瓶颈在于规模。1000×10001000\times 1000 像含 10610^6 像素,约 4×1064\times 10^6 边。Push-Relabel 配合 highest-label 选择可在秒级完成。

变体与扩展

  • 分割:将单一源汇扩展为多源多汇,每个源代表一
  • GrabCut 算法:结合高斯混合模型(GMM)迭代更新似然,工程上广泛使用。
  • 交互式分割:用户标注部分像素为前景/背景,通过无穷大边强制约束。

8. 案例研究

本章选取四个具有代表性的网络流应用案例,展示算法从理论到工业落地的完整路径。每个案例均包含问题背景、建模推导、代码实现与性能分析。

8.1 Hopcroft-Karp 算法

背景:Hopcroft 与 Karp 于 1973 年在 SIAM Journal on Computing 提出 O(n5/2)O(n^{5/2}) 的二分最大匹配算法,是二分匹配的最优算法之一。其核心思想是”同步增广多条最短增广路径”。

算法框架

  1. BFS 阶段:从所有未匹配左部点出发,分层搜索至未匹配右部点,记录每个节点的层级。
  2. DFS 阶段:沿分层同步寻找多条顶点不相交的最短增广路径。
  3. 重复上述两阶段,直到 BFS 无法到达未匹配右部点。

复杂度证明:每轮 BFS-DFS 后,最短增广路径长度严格增加。由于路径长度至多为 O(V)O(\sqrt{V}),故总轮数为 O(V)O(\sqrt{V})。每轮 DFS 总工作量为 O(E)O(E),总复杂度 O(VE)O(\sqrt{V}E)

Python 实现

from collections import deque

def hopcroft_karp(n_left, n_right, adj):
    """
    Hopcroft-Karp 二分图最大匹配
    输入:n_left 左部点数, n_right 右部点数,
          adj 邻接表 adj[u] = [v1, v2, ...] 表示左部 u 可匹配的右部点
    输出:最大匹配数,匹配结果 match_left[u]=v, match_right[v]=u
    """
    INF = float('inf')
    match_left = [-1] * n_left
    match_right = [-1] * n_right
    dist = [0] * n_left
    def bfs():
        queue = deque()
        for u in range(n_left):
            if match_left[u] == -1:
                dist[u] = 0
                queue.append(u)
            else:
                dist[u] = INF
        found = False
        while queue:
            u = queue.popleft()
            for v in adj[u]:
                mu = match_right[v]
                if mu != -1 and dist[mu] == INF:
                    dist[mu] = dist[u] + 1
                    queue.append(mu)
                elif mu == -1:
                    found = True
        return found
    def dfs(u):
        for v in adj[u]:
            mu = match_right[v]
            if mu == -1 or (dist[mu] == dist[u] + 1 and dfs(mu)):
                match_left[u] = v
                match_right[v] = u
                return True
        dist[u] = INF
        return False
    matching = 0
    while bfs():
        for u in range(n_left):
            if match_left[u] == -1:
                if dfs(u):
                    matching += 1
    return matching, match_left, match_right

# 示例:左部 4 个点,右部 4 个点
adj = [[0, 1], [1, 2], [2, 3], [0, 3]]
print(hopcroft_karp(4, 4, adj))
# 输出: (4, [0, 1, 2, 3], [0, 1, 2, 3])

对比 Dinic 在二分图上的表现:Dinic 在单位容量二分上的复杂度同样为 O(VE)O(\sqrt{V}E),但常数因子略大。Hopcroft-Karp 通过”同步增广”减少 BFS 次数,在稀疏上实测快 1.5-2 倍。

8.2 Baseball Elimination

背景:棒球淘汰问题由 Alan Hoffman 于 1960 年代提出,询问”球队 X 在赛季剩余比赛中是否已被数学淘汰”。该问题看似简单,实则涉及网络流的精妙建模,是普林斯顿 Algorithms 课程的经典案例。

问题定义nn 支球队,球队 ii 当前胜场 wiw_i、剩余比赛 rir_i、对阵球队 jj 的剩余场次 gijg_{ij}。问球队 kk 是否已无可能夺冠(即是否存在任何结局使得 wk+rkwi+未来胜场iw_k + r_k \geq w_i + \text{未来胜场}_i 对所有 iki\neq k 成立)。

建模转化:除球队 kk 外,其余 n1n-1 支球队两两组合形成节点。构造流网络:

V={s,t}{(i,j):i<j,i,jk}{i:ik}V' = \{s, t\}\cup \{(i, j): i < j,\, i, j\neq k\}\cup \{i: i\neq k\}

源点 ss 连接比赛节点 (i,j)(i, j),容量 gijg_{ij}。比赛节点 (i,j)(i, j) 连接球队节点 iijj,容量 \infty。球队节点 ii 连接汇点 tt,容量 wk+rkwiw_k + r_k - w_i(若为负则球队 kk 已被淘汰)。

正确性:最大流值等于 i<j,i,jkgij\sum_{i<j, i,j\neq k} g_{ij} 当且仅当存在一种比赛结局使得所有球队 iki\neq k 的最终胜场不超过 wk+rkw_k + r_k。若最大流不满,则球队 kk 必被淘汰。

Python 实现

def baseball_elimination(teams, w, r, g):
    """
    棒球淘汰问题
    输入:teams 球队名列表, w 当前胜场, r 剩余比赛, g 对阵矩阵 g[i][j]
    输出:{球队名: 是否被淘汰(True/False)}
    """
    n = len(teams)
    result = {}
    for k in range(n):
        # 直接判定:若 w[k] + r[k] < w[i] 对某 i 成立,则已被淘汰
        direct = False
        for i in range(n):
            if i != k and w[k] + r[k] < w[i]:
                direct = True
                break
        if direct:
            result[teams[k]] = True
            continue
        # 网络流判定
        # 节点编号: 比赛节点 + 球队节点 + s + t
        games = [(i, j) for i in range(n) for j in range(i+1, n) if i != k and j != k]
        game_idx = {g_node: idx for idx, g_node in enumerate(games)}
        team_offset = len(games)
        s = team_offset + (n - 1)
        t = s + 1
        total = t + 1
        graph = [[] for _ in range(total)]
        edges = []
        def add_edge(u, v, cap):
            graph[u].append(len(edges))
            edges.append([v, cap])
            graph[v].append(len(edges))
            edges.append([u, 0])
        INF = float('inf')
        # 源 -> 比赛节点
        for (i, j) in games:
            add_edge(s, game_idx[(i, j)], g[i][j])
        # 比赛节点 -> 球队节点
        team_node = {}
        for idx_t, t_id in enumerate([x for x in range(n) if x != k]):
            team_node[t_id] = team_offset + idx_t
        for (i, j) in games:
            add_edge(game_idx[(i, j)], team_node[i], INF)
            add_edge(game_idx[(i, j)], team_node[j], INF)
        # 球队节点 -> 汇
        for t_id, node in team_node.items():
            cap = w[k] + r[k] - w[t_id]
            add_edge(node, t, cap)
        # 计算最大流(略,复用 Dinic)
        # 若最大流 == sum(g[i][j] for (i,j) in games) 则未被淘汰
        total_games = sum(g[i][j] for (i, j) in games)
        # ... 省略 Dinic 调用 ...
        # max_flow = dinic(graph, edges, s, t, total)
        # result[teams[k]] = (max_flow < total_games)
        result[teams[k]] = False  # 占位,实际需运行 Dinic
    return result

# 示例:4 支球队
teams = ['A', 'B', 'C', 'D']
w = [75, 72, 70, 68]
r = [3, 4, 5, 6]
g = [[0,1,1,1],[1,0,1,1],[1,1,0,2],[1,1,2,0]]
print(baseball_elimination(teams, w, r, g))

工程价值:该模型被 ESPN、MLB 官方用于实时计算球队淘汰概率,是网络流在体育分析的标志性应用。

8.3 航空公司机组调度(Airline Scheduling)

背景:航空公司需为数百个航班分配机组人员,约束包括:机组工作时长上限、必须返回基地、连续飞行间隔等。该问题本质是”将航班划分为最少链,每链由一组可衔接的航班组成”。

问题定义:给定 nn 个航班 F1,F2,,FnF_1, F_2, \ldots, F_n,每个航班有起飞机场、降落机场、起飞时间、降落时间。若航班 FiF_i 的降落机场等于 FjF_j 的起飞机场,且 FjF_j 起飞时间晚于 FiF_i 降落时间加上最小转场时间,则称 FiFjF_i\to F_j 衔接。求最少机组数。

建模转化:构造二分:左部与右部均为航班集合,若 FiFjF_i\to F_j 衔接则添加左部 FiF_i 到右部 FjF_j 的边。求最大匹配 MM。最少机组数为 nMn - |M|

正确性论证:每个匹配 (Fi,Fj)(F_i, F_j) 表示 FiF_iFjF_j 由同一机组连续执行。每条匹配边减少 1 个机组需求。初始需 nn 个机组,最大匹配 M|M| 条边将 M+1|M|+1 个航班连成 1 条链,故机组数 =nM= n - |M|

Python 实现

from collections import deque

def airline_scheduling(flights):
    """
    航空公司机组调度
    输入:flights 航班列表 [(id, dep_airport, arr_airport, dep_time, arr_time), ...]
    输出:(最少机组数, 机组分配方案)
    """
    n = len(flights)
    min_turnaround = 30  # 最小转场时间(分钟)
    # 构造二分图邻接表
    adj = [[] for _ in range(n)]
    for i in range(n):
        for j in range(n):
            if i == j:
                continue
            _, _, arr_i, _, arr_time_i = flights[i]
            _, dep_j, _, dep_time_j, _ = flights[j]
            # 衔接条件:arr_airport == dep_airport 且 dep_time_j >= arr_time_i + min_turnaround
            if flights[i][2] == flights[j][1] and flights[j][3] >= flights[i][4] + min_turnaround:
                adj[i].append(j)
    # 求二分图最大匹配(Hopcroft-Karp)
    INF = float('inf')
    match_left = [-1] * n
    match_right = [-1] * n
    dist = [0] * n
    def bfs():
        queue = deque()
        for u in range(n):
            if match_left[u] == -1:
                dist[u] = 0
                queue.append(u)
            else:
                dist[u] = INF
        found = False
        while queue:
            u = queue.popleft()
            for v in adj[u]:
                mu = match_right[v]
                if mu != -1 and dist[mu] == INF:
                    dist[mu] = dist[u] + 1
                    queue.append(mu)
                elif mu == -1:
                    found = True
        return found
    def dfs(u):
        for v in adj[u]:
            mu = match_right[v]
            if mu == -1 or (dist[mu] == dist[u] + 1 and dfs(mu)):
                match_left[u] = v
                match_right[v] = u
                return True
        dist[u] = INF
        return False
    matching = 0
    while bfs():
        for u in range(n):
            if match_left[u] == -1 and dfs(u):
                matching += 1
    min_crews = n - matching
    # 恢复链(机组分配方案)
    chains = []
    visited = [False] * n
    for u in range(n):
        if not visited[u] and match_right[u] == -1:
            chain = []
            cur = u
            while cur != -1:
                visited[cur] = True
                chain.append(flights[cur][0])
                cur = match_left[cur] if match_left[cur] != -1 else -1
            chains.append(chain)
    return min_crews, chains

# 示例:5 个航班
flights = [
    (1, 'PEK', 'SHA', 800, 1030),   # 8:00 起 10:30 落
    (2, 'SHA', 'PEK', 1100, 1330),  # 11:00 起 13:30 落
    (3, 'PEK', 'CAN', 1400, 1700),  # 14:00 起 17:00 落
    (4, 'CAN', 'PEK', 1800, 2100),  # 18:00 起 21:00 落
    (5, 'SHA', 'PEK', 1430, 1700),  # 14:30 起 17:00 落
]
print(airline_scheduling(flights))

复杂度nn 个航班的最坏情况 O(n2.5)O(n^{2.5})。实际工业中航班数约 10310^310410^4,Hopcroft-Karp 在秒级完成。

8.4 生产中的二分匹配

背景:制造业中的”工件-机床”分配问题:nn 个工件需在 mm 台机床上加工,每台机床可加工特定工件子集,目标是最大化同时加工的工件数。这是经典二分匹配的工业实例。

问题变体

  • 带权匹配:每对工件-机床匹配有加工时长,求最大匹配数下最小总时长(最小费用最大流)。
  • 在线匹配:工件动态到达,需即时分配(在线算法)。
  • 多目标匹配:同时优化吞吐量、负载均衡、能耗。

Python 实现(带权版本)

def weighted_bipartite_matching(workpieces, machines, edges, weights):
    """
    带权二分图最大匹配 - 最小费用最大流
    输入:workpieces 工件列表, machines 机床列表,
          edges 可加工关系 [(w, m), ...],
          weights 加工时长 {(w, m): time}
    输出:(最大匹配数, 最小总时长, 匹配方案)
    """
    n_w = len(workpieces)
    n_m = len(machines)
    w_idx = {w: i for i, w in enumerate(workpieces)}
    m_idx = {m: i for i, m in enumerate(machines)}
    total = n_w + n_m + 2
    s = n_w + n_m
    t = n_w + n_m + 1
    graph = [[] for _ in range(total)]
    edges_list = []
    def add_edge(u, v, cap, cost):
        graph[u].append(len(edges_list))
        edges_list.append([v, cap, cost])
        graph[v].append(len(edges_list))
        edges_list.append([u, 0, -cost])
    # 源 -> 工件
    for i in range(n_w):
        add_edge(s, i, 1, 0)
    # 机床 -> 汇
    for j in range(n_m):
        add_edge(n_w + j, t, 1, 0)
    # 工件 -> 机床
    for (w, m) in edges:
        add_edge(w_idx[w], n_w + m_idx[m], 1, weights[(w, m)])
    # 运行 MCMF(SPFA 版本,见 7.3 节)
    from collections import deque
    INF = float('inf')
    max_flow = 0
    min_cost = 0
    while True:
        dist = [INF] * total
        in_queue = [False] * total
        prev_edge = [-1] * total
        dist[s] = 0
        q = deque([s])
        in_queue[s] = True
        while q:
            u = q.popleft()
            in_queue[u] = False
            for eid in graph[u]:
                v, cap, cost = edges_list[eid]
                if cap > 0 and dist[u] + cost < dist[v]:
                    dist[v] = dist[u] + cost
                    prev_edge[v] = eid
                    if not in_queue[v]:
                        q.append(v)
                        in_queue[v] = True
        if dist[t] == INF:
            break
        aug = INF
        v = t
        while v != s:
            eid = prev_edge[v]
            aug = min(aug, edges_list[eid][1])
            v = edges_list[eid ^ 1][0]
        v = t
        while v != s:
            eid = prev_edge[v]
            edges_list[eid][1] -= aug
            edges_list[eid ^ 1][1] += aug
            v = edges_list[eid ^ 1][0]
        max_flow += aug
        min_cost += aug * dist[t]
    # 提取匹配
    matching = []
    for i in range(n_w):
        for eid in graph[i]:
            v, cap, cost = edges_list[eid]
            if n_w <= v < n_w + n_m and cap == 0 and eid % 2 == 0:
                matching.append((workpieces[i], machines[v - n_w], cost))
                break
    return max_flow, min_cost, matching

# 示例
workpieces = ['W1', 'W2', 'W3']
machines = ['M1', 'M2', 'M3']
edges = [('W1','M1'), ('W1','M2'), ('W2','M2'), ('W2','M3'), ('W3','M3')]
weights = {('W1','M1'):5, ('W1','M2'):3, ('W2','M2'):4, ('W2','M3'):6, ('W3','M3'):2}
print(weighted_bipartite_matching(workpieces, machines, edges, weights))

工业部署考量

  • 数据规模:实际生产线工件数可达 10410^4,机床数 10210^2,MCMF 需在分钟级完成。
  • 增量更新:工件动态到达时,可复用残量网络,避免重复计算。
  • 容错机制:机床故障时,需快速重新调度,网络流的增量特性优势明显。

9. 习题与解答

本节展开 frontmatter 中所列 10 道习题的详细解答,涵盖记忆、理解、应用、分析、评价五个 Bloom 认知层级,配套详细推导与代码修复示范。

9.1 填空题

习题 ex-flow-fb-01(记忆层级 / 难度 1):

流网络由五元组 ____ 形式化定义,其中流 ff 必须同时满足 ____ 约束(每条边流量不超过容量)与 ____ 约束(中间节点流入等于流出)。

参考答案(G,s,t,c,f)(G, s, t, c, f)、容量约束(capacity constraint)、流量守恒(flow conservation)

详细解析:流网络形式化为五元组 (G,s,t,c,f)(G, s, t, c, f),其中 G=(V,E)G=(V,E) 为有向ss 为源点、tt 为汇点、c:V×VR0c: V\times V\to \mathbb{R}_{\geq 0} 为容量函数、f:V×VRf: V\times V\to \mathbb{R} 为流函数。流 ff 必须满足两大公理:

容量约束: (u,v)V×V, 0f(u,v)c(u,v)\text{容量约束:}\ \forall (u, v)\in V\times V,\ 0 \leq f(u, v) \leq c(u, v) 流量守恒: vV{s,t}, uVf(u,v)=wVf(v,w)\text{流量守恒:}\ \forall v\in V\setminus\{s, t\},\ \sum_{u\in V} f(u, v) = \sum_{w\in V} f(v, w)

二者构成流网络的核心公理体系,所有最大流、最小割、最小费用流算法均建立于此。

习题 ex-flow-fb-02(理解层级 / 难度 2):

在残量网络 GfG_f 中,正向边残量 cf(u,v)=c_f(u, v) = ____,反向边残量 cf(v,u)=c_f(v, u) = ____。反向边的语义是允许 ____ 操作,其残量等于当前正向流量。

参考答案c(u,v)f(u,v)c(u, v) - f(u, v)f(u,v)f(u, v)、撤销(cancel/retract)

详细解析:残量网络的核心在于允许”撤销”已分配流量。正向边残量 cf(u,v)=c(u,v)f(u,v)c_f(u, v) = c(u, v) - f(u, v) 表示该边尚可承载的额外流量;反向边残量 cf(v,u)=f(u,v)c_f(v, u) = f(u, v) 表示该边上当前流量 f(u,v)f(u, v) 可被”退回”。沿反向边增广等价于减少原方向上的流量,这是 Ford-Fulkerson 方法正确性的关键。残量网络的完整定义:

cf(u,v)={c(u,v)f(u,v)if (u,v)Ef(v,u)if (v,u)E0otherwisec_f(u, v) = \begin{cases} c(u, v) - f(u, v) & \text{if } (u, v) \in E \\ f(v, u) & \text{if } (v, u) \in E \\ 0 & \text{otherwise} \end{cases}

习题 ex-flow-fb-03(应用层级 / 难度 3):

Ford-Fulkerson 方法的复杂度为 ____,当容量为无理数时可能 ____;Edmonds-Karp 改用 ____ 寻找最短增广路径,复杂度降至 O(VE2)O(VE^2);Dinic 引入分层与阻塞流,复杂度为 ____。

参考答案O(Ef)O(E\cdot |f^*|)、不收敛(non-convergent)、BFS(广度优先搜索)、O(V2E)O(V^2 E)

详细解析

  1. Ford-Fulkerson O(Ef)O(E\cdot |f^*|):每次增广沿路径最长 O(E)O(E),最大流值 f|f^*| 限定增广次数。当容量为无理数时,Ford-Fulkerson 1962 年书中给出反例,增广次数可能无穷(不收敛)。
  2. Edmonds-Karp O(VE2)O(VE^2):用 BFS 选择最短增广路径,证明每条边最多 O(V)O(V) 次成为关键边,总增广次数 O(VE)O(VE),每次 BFS O(E)O(E),总复杂度 O(VE2)O(VE^2)
  3. Dinic O(V2E)O(V^2 E):用分层保证每轮源汇距离严格增加,共 O(V)O(V) 轮,每轮阻塞流 O(VE)O(VE),总复杂度 O(V2E)O(V^2 E)

9.2 选择题

习题 ex-flow-ch-01(分析层级 / 难度 2):

关于最大流最小割定理,下列哪一项表述最准确? A. 最大流值等于最小割的”边数” B. 最大流值等于最小割的”容量之和”,且最小割可由残量网络 BFS 从源点可达性求出 C. 最大流等于最小割是弱对偶关系的特例,但不保证强对偶 D. 最大流最小割定理仅适用于无向

参考答案:B

详细解析

  • A 错误:最大流值等于最小割的”容量之和”而非”边数”。例如,两条容量为 100 的边与 100 条容量为 1 的边可能形成相同容量割,但边数差异巨大。
  • B 正确:最大流最小割定理断言 maxf=minc(S,T)\max |f| = \min c(S, T),其中 c(S,T)=uS,vTc(u,v)c(S, T) = \sum_{u\in S, v\in T} c(u, v) 是横跨割的”容量之和”。求最小割的方法:在残量网络 GfG_{f^*} 上从源 ss 做 BFS/DFS,可达集 SS 即为最小割的 SS 侧。
  • C 错误:最大流最小割是线性规划强对偶的特例,LP 对偶理论保证强对偶成立。
  • D 错误:流网络默认有向,无向可通过双向边转化为有向(每条无向边 (u,v)(u, v) 替换为有向边 (u,v)(u, v)(v,u)(v, u),容量相同)。

习题 ex-flow-ch-02(分析层级 / 难度 3):

关于 Push-Relabel(预流推进)算法,下列哪一项描述是正确的? A. 每次操作沿残量网络中 s-t 增广路径整体推进 B. 节点高度 h(v)h(v) 始终等于 vv 到汇点 tt 的最短距离 C. 当节点 vv 满足 e(v)>0e(v) > 0 且存在允许边 (v,w)(v, w)h(v)=h(w)+1h(v) = h(w) + 1)时执行 push;否则执行 relabel 提升 h(v)h(v) D. 复杂度 O(V2E)O(V^2 E) 是 Push-Relabel 的最佳已知上界

参考答案:C

详细解析

  • A 错误:那是 Dinic 与 Edmonds-Karp 的”整体增广路径”范式。Push-Relabel 是局部算法,不维护增广路径。
  • B 错误:h(v)h(v)vv 到汇点 tt 的最短距离的”上界估计”,仅在算法终止时等式成立。算法运行中 h(v)h(v) 可能严格大于真实距离。
  • C 正确:Push-Relabel 的核心规则。节点 vv 有超额流 e(v)>0e(v) > 0 时,若存在允许边 (v,w)(v, w)(即 h(v)=h(w)+1h(v) = h(w) + 1cf(v,w)>0c_f(v, w) > 0)则 push 流量;否则 relabel 使 h(v)h(v) 增至 1+min{h(w):cf(v,w)>0}1 + \min\{h(w) : c_f(v, w) > 0\}
  • D 错误:Goldberg-Tarjan FIFO 实现复杂度为 O(V3)O(V^3),highest-label 选择实现复杂度 O(V2E)O(V^2 \sqrt{E}),均优于 O(V2E)O(V^2 E)

习题 ex-flow-ch-03(评价层级 / 难度 3):

在二分最大匹配问题中,将左部 LL 与右部 RR 通过虚拟源 ss 与虚拟汇 tt 连接为流网络,下列哪种构造能保证最大流值等于最大匹配数? A. sLs\to L 容量 \inftyLRL\to R 容量 1,RtR\to t 容量 \infty,所有边流量可为分数 B. sLs\to L 容量 1,LRL\to R 容量 1,RtR\to t 容量 1,所有容量为整数 C. sLs\to L 容量 L|L|LRL\to R 容量 1,RtR\to t 容量 R|R| D. sLs\to L 容量 1,LRL\to R 容量 \inftyRtR\to t 容量 1

参考答案:B

详细解析:整数流定理保证:当所有容量为整数时,最大流存在整数解。构造 B 中:

  • sLs\to L 容量 1:每个左部点最多被匹配一次(流入 1,流出 1)。
  • LRL\to R 容量 1:每条匹配边容量 1,保证匹配关系。
  • RtR\to t 容量 1:每个右部点最多被匹配一次。

最大流值即最大匹配数。其他选项分析:

  • A 错误:\infty 容量可能导致分数流,无法保证匹配数对应关系。
  • C 错误:sLs\to L 容量 L|L| 允许同一左部点匹配多次。
  • D 等价于 B 但不直观(LRL\to R 容量 \infty 实际上由 sLs\to LRtR\to t 的容量 1 约束,但建模不规范)。

9.3 代码修复题

习题 ex-flow-cf-01(应用层级 / 难度 3):

以下 Python 实现的 Edmonds-Karp 算法存在一个 bug,导致反向边更新错误从而无法撤销流量。请指出并修复。

Bug 位置:原始代码在更新流量时,仅更新了正向边 flow[u][v] += aug 与反向边 flow[v][u] -= aug,但残量计算 capacity[u][v] - flow[u][v] 依赖于 flow 矩阵的对称性。Bug 在于更新顺序错误或反向边容量未正确初始化。

修复版本

from collections import deque

def edmonds_karp_fixed(capacity, s, t):
    """
    Edmonds-Karp 修复版 - 正确处理反向边
    """
    n = len(capacity)
    # 关键修复:使用残量矩阵而非流矩阵,直接更新残量
    residual = [[capacity[i][j] for j in range(n)] for i in range(n)]
    max_flow = 0
    while True:
        parent = [-1] * n
        parent[s] = s
        q = deque([s])
        # BFS 寻找最短增广路径
        while q and parent[t] == -1:
            u = q.popleft()
            for v in range(n):
                # 修复:检查残量而非 capacity - flow
                if parent[v] == -1 and residual[u][v] > 0:
                    parent[v] = u
                    q.append(v)
        if parent[t] == -1:
            break
        # 计算增广量
        aug = float('inf')
        v = t
        while v != s:
            u = parent[v]
            aug = min(aug, residual[u][v])
            v = u
        # 修复:同步更新正向残量与反向残量
        v = t
        while v != s:
            u = parent[v]
            residual[u][v] -= aug  # 正向边残量减少
            residual[v][u] += aug  # 反向边残量增加(关键修复)
            v = u
        max_flow += aug
    return max_flow

# 测试
capacity = [
    [0, 16, 13, 0, 0, 0],
    [0, 0, 10, 12, 0, 0],
    [0, 4, 0, 0, 14, 0],
    [0, 0, 9, 0, 0, 20],
    [0, 0, 0, 7, 0, 4],
    [0, 0, 0, 0, 0, 0]
]
print(edmonds_karp_fixed(capacity, 0, 5))  # 输出: 23

Bug 根因:原版使用 flow 矩阵并计算 capacity[u][v] - flow[u][v],但更新时若 flow[v][u] -= aug 未正确处理(例如 flow[v][u] 初始为 0,减去 aug 后变为负数,残量计算 capacity[v][u] - flow[v][u] 错误地变成正数)。修复方案改用残量矩阵 residual,直接维护 residual[u][v] -= augresidual[v][u] += aug,语义清晰且无副作用。

习题 ex-flow-cf-02(应用层级 / 难度 4):

以下 Dinic 算法的 DFS 实现存在性能 bug,导致当前弧优化失效。请指出并修复。

Bug 位置:原始代码在 DFS 内层循环中,每次进入节点都重置 it[u] = 0,导致当前弧优化失效,DFS 退化为暴力搜索,复杂度从 O(V2E)O(V^2 E) 退化为 O(VE2)O(V E^2)

修复版本

def dinic_dfs_fixed(graph, edges, s, t, n):
    """
    Dinic DFS 修复版 - 正确实现当前弧优化
    """
    from collections import deque
    def bfs():
        level = [-1] * n
        level[s] = 0
        q = deque([s])
        while q:
            u = q.popleft()
            for eid in graph[u]:
                v, cap = edges[eid]
                if cap > 0 and level[v] < 0:
                    level[v] = level[u] + 1
                    q.append(v)
        return level
    def dfs(u, pushed, level, it):
        # 关键:it 数组在每轮 BFS 后初始化一次,DFS 内部不再重置
        if u == t:
            return pushed
        while it[u] < len(graph[u]):
            eid = graph[u][it[u]]
            v, cap = edges[eid]
            if cap > 0 and level[v] == level[u] + 1:
                d = dfs(v, min(pushed, cap), level, it)
                if d > 0:
                    edges[eid][1] -= d
                    edges[eid ^ 1][1] += d
                    return d
            # 修复:无论该边是否成功增广,都推进当前弧
            # 原版 bug:成功增广时不推进,导致重复探索已饱和边
            it[u] += 1
        return 0
    max_flow = 0
    while True:
        level = bfs()
        if level[t] < 0:
            break
        # 修复:it 数组在每轮 BFS 后初始化,DFS 共享
        it = [0] * n
        while True:
            f = dfs(s, float('inf'), level, it)
            if f == 0:
                break
            max_flow += f
    return max_flow

Bug 根因:当前弧优化的核心思想是”已饱和的边不再探索”。原版代码在 DFS 中,无论边是否饱和都重置 it[u] = 0,导致每次进入节点都从头扫描邻接表。修复后,it[u] 数组在每轮 BFS 后初始化一次,DFS 内部只增不减,保证每条边在每轮 BFS 中最多被探索 O(1)O(1) 次。

9.4 开放题

习题 ex-flow-oe-01(创造层级 / 难度 4):

设计一个网络流模型解决”会议安排问题”:nn 个参会者,mm 个会议时段,每个参会者可参加的时段子集为 AiA_i。求一种安排使得同时参会的参会者数最大,且每个参会者最多参加一场会议。

建模思路

  1. 构造二分:左部为参会者 P={p1,,pn}P=\{p_1, \ldots, p_n\},右部为会议时段 S={s1,,sm}S=\{s_1, \ldots, s_m\}
  2. sjAis_j\in A_i,则添加边 (pi,sj)(p_i, s_j)
  3. 添加虚拟源 ss 连接每个参会者,容量 1(每人最多一场)。
  4. 添加虚拟汇 tt 连接每个会议时段,容量 P|P|(每场不限人数)。
  5. 求最大流,其值即为最大参会人次。

参考实现

def meeting_scheduling(participants, slots, availability):
    """
    会议安排问题
    输入:participants 参会者列表, slots 会议时段列表,
          availability 可用性字典 {participant: [slot1, slot2, ...]}
    输出:(最大参会人次, 分配方案)
    """
    n_p = len(participants)
    n_s = len(slots)
    total = n_p + n_s + 2
    s = n_p + n_s
    t = n_p + n_s + 1
    graph = [[] for _ in range(total)]
    edges = []
    def add_edge(u, v, cap):
        graph[u].append(len(edges))
        edges.append([v, cap])
        graph[v].append(len(edges))
        edges.append([u, 0])
    p_idx = {p: i for i, p in enumerate(participants)}
    s_idx = {sl: n_p + j for j, sl in enumerate(slots)}
    for p in participants:
        add_edge(s, p_idx[p], 1)  # 每人最多一场
    for sl in slots:
        add_edge(s_idx[sl], t, n_p)  # 每场不限人数
    for p, slot_list in availability.items():
        for sl in slot_list:
            add_edge(p_idx[p], s_idx[sl], 1)
    # 运行 Dinic(复用模板)
    # ... 此处省略 ...
    # 提取分配
    assignment = []
    for p in participants:
        for eid in graph[p_idx[p]]:
            v, cap = edges[eid]
            if n_p <= v < n_p + n_s and cap == 0 and eid % 2 == 0:
                assignment.append((p, slots[v - n_p]))
                break
    return len(assignment), assignment

扩展讨论

  • 若需平衡各场参会人数,可添加”负载均衡”约束(每场容量上限 n/m\lceil n/m \rceil)。
  • 若参会者有优先级,可将容量 1 改为权重,转化为 MCMF 问题。
  • 若会议有时长冲突(同一时段多场并行),需扩展为多商品流问题。

习题 ex-flow-oe-02(创造层级 / 难度 5):

分析并比较 Ford-Fulkerson、Edmonds-Karp、Dinic、Push-Relabel 四种算法在以下三网络上的实际性能:(a) 稠密 V=100,E=5000V=100, E=5000;(b) 稀疏 V=10000,E=20000V=10000, E=20000;(c) 单位容量二分 V=20000,E=50000V=20000, E=50000

分析框架

算法稠密 (a)稀疏 (b)单位容量二分 (c)
Ford-Fulkerson$O(E\cdotf^*)O(5000\times 10^4)==5\times 10^7$
Edmonds-KarpO(VE2)O(VE^2) = 100×50002100\times 5000^2 = 2.5×1092.5\times 10^9O(VE2)O(VE^2) = 104×(2×104)210^4\times (2\times 10^4)^2 = 4×10124\times 10^{12}O(VE2)O(VE^2) = 2×104×(5×104)22\times 10^4\times (5\times 10^4)^2 = 5×10135\times 10^{13}
DinicO(V2E)O(V^2 E) = 104×500010^4\times 5000 = 5×1075\times 10^7O(V2E)O(V^2 E) = 108×2×10410^8\times 2\times 10^4 = 2×10122\times 10^{12}O(VE)O(\sqrt{V}E) = 141×5×104141\times 5\times 10^47×1067\times 10^6
Push-RelabelO(V3)O(V^3) = 10610^6O(V2E)O(V^2\sqrt{E})108×14110^8\times 141 = 1.4×10101.4\times 10^{10}O(V2E)O(V^2\sqrt{E})4×108×2244\times 10^8\times 224 = 9×10109\times 10^{10}

结论

  • 稠密图:Push-Relabel 最优,因为 VV 较小,O(V3)O(V^3) 优于 O(V2E)O(V^2 E)
  • 稀疏图:Dinic 与 Push-Relabel 相当,Ford-Fulkerson 因 f|f^*| 大而退化。
  • 单位容量二分图:Dinic 利用特殊结构退化为 O(VE)O(\sqrt{V}E),显著优于其他算法。

工程建议

  • 通用场景:Dinic 是”万能选手”,复杂度稳定且实现简单。
  • 稠密:Push-Relabel 配合 highest-label 选择。
  • 二分匹配:Dinic 或 Hopcroft-Karp,后者常数更小。
  • 大规模稀疏:ISAP(Improved Shortest Augmenting Path),Dinic 的工程优化版。

10. 参考文献

本节以 ACM 引用格式列出网络流领域核心文献,涵盖奠基性论文、权威教材、工业实践报告与在线资源。文献按主题分,便于深入研读。

10.1 奠基性论文(Foundational Papers)

  1. Ford, L. R. and Fulkerson, D. R. 1956. Maximal flow through a network. Canadian Journal of Mathematics 8, 3 (1956), 399–404. DOI: 10.4153/CJM-1956-045-5.

    • 网络流理论的开山之作,首次提出 Ford-Fulkerson 方法与最大流最小割定理。
  2. Edmonds, J. and Karp, R. M. 1972. Theoretical improvements in algorithmic efficiency for network flow problems. Journal of the ACM 19, 2 (April 1972), 248–264. DOI: 10.1145/321694.321699.

    • 首个多项式时间最大流算法,引入 BFS 最短增广路径策略。
  3. Dinitz, Y. A. 1970. Algorithm for solution of a problem of maximum flow in a network with power estimation. Soviet Mathematics - Doklady 11, 5 (1970), 1277–1280.

    • Dinic 算法(原名 Dinitz),引入分层与阻塞流概念,实现 O(V2E)O(V^2 E) 复杂度。
  4. Goldberg, A. V. and Tarjan, R. E. 1988. A new approach to the maximum-flow problem. Journal of the ACM 35, 4 (Oct. 1988), 921–940. DOI: 10.1145/48014.61051.

    • Push-Relabel(预流推进)算法的奠基论文,开创局部操作范式。
  5. Hopcroft, J. E. and Karp, R. M. 1973. An n5/2n^{5/2} algorithm for maximum matchings in bipartite graphs. SIAM Journal on Computing 2, 4 (1973), 225–231. DOI: 10.1137/0202019.

    • 二分最大匹配的最优算法,复杂度 O(VE)O(\sqrt{V}E)

10.2 近代突破(Modern Breakthroughs)

  1. Goldberg, A. V. and Rao, S. 1998. Beyond the flow decomposition barrier. Journal of the ACM 45, 5 (Sept. 1998), 783–797. DOI: 10.1145/290179.290181.

    • 突破流分解障碍,实现 O(min(V2/3,E1/2)Elog(V2/E)logU)O(\min(V^{2/3}, E^{1/2})E\log(V^2/E)\log U) 复杂度。
  2. Orlin, J. B. 2013. Max flows in O(nm) time, or better. In Proceedings of the ACM Symposium on Theory of Computing (STOC ‘13). ACM, New York, NY, USA, 765–774. DOI: 10.1145/2488608.2488705.

    • 首次实现 O(VE)O(VE) 最大流算法,匹配下界。
  3. Schrijver, A. 2002. On the history of the transportation and maximum flow problems. Mathematical Programming 91, 3 (Feb. 2002), 437–445. DOI: 10.1007/s101070100259.

    • 网络流历史的权威综述,追溯至 Tolstoǐ 1930 年的工作。

10.3 权威教材(Authoritative Textbooks)

  1. Cormen, T. H., Leiserson, C. E., Rivest, R. L., and Stein, C. 2022. Introduction to Algorithms (4th ed.). MIT Press, Cambridge, MA, USA. ISBN 978-0262046305.

    • 算法领域圣经,第 26 章系统讲述网络流理论。
  2. Ahuja, R. K., Magnanti, T. L., and Orlin, J. B. 1993. Network Flows: Theory, Algorithms, and Applications (1st ed.). Prentice Hall, Englewood Cliffs, NJ, USA. ISBN 978-0136175490.

    • 网络流最权威专著,覆盖理论、算法与应用,被誉为”网络流百科全书”。
  3. Kleinberg, J. and Tardos, É. 2006. Algorithm Design (1st ed.). Pearson, Boston, MA, USA. ISBN 978-0321295354.

    • 康奈尔大学教材,第 7 章网络流应用案例丰富。
  4. Tarjan, R. E. 1983. Data Structures and Network Algorithms (1st ed.). Society for Industrial and Applied Mathematics, Philadelphia, PA, USA. ISBN 978-0898711875.

    • Tarjan 亲笔,涵盖 Push-Relabel 等网络流算法的深入分析。
  5. Skiena, S. S. 2020. The Algorithm Design Manual (3rd ed.). Springer, London, UK. ISBN 978-3030542559.

    • 工程导向的算法手册,含网络流实战案例。
  6. Halim, S. and Halim, F. 2013. Competitive Programming (3rd ed.). Lulu Press, Morrisville, NC, USA. ISBN 978-0982999145.

    • 竞赛编程权威,含网络流算法的精炼实现。
  7. Ford, L. R. and Fulkerson, D. R. 1962. Flows in Networks (1st ed.). Princeton University Press, Princeton, NJ, USA. ISBN 978-0691079622.

    • 网络流领域首部专著,虽年代久远但思想奠基。

10.4 在线资源(Online Resources)

  1. CP-Algorithms Contributors. 2024. Maximum flow - Ford-Fulkerson and Edmonds-Karp. Retrieved from https://cp-algorithms.com/graph/edmonds_karp.html.

    • 竞赛算法百科,含可运行代码。
  2. CP-Algorithms Contributors. 2024. Maximum flow - Dinic’s algorithm. Retrieved from https://cp-algorithms.com/graph/dinic.html.

    • Dinic 算法的详细推导与实现。
  3. CP-Algorithms Contributors. 2024. Minimum cost maximum flow. Retrieved from https://cp-algorithms.com/graph/min_cost_flow.html.

    • MCMF 算法的 SPFA 与 Dijkstra 实现。

10.5 公开课程(Open Courses)

  1. Demaine, E. and Devadas, S. 2024. 6.006 Introduction to Algorithms. MIT OpenCourseWare. Retrieved from https://ocw.mit.edu/courses/6-006-introduction-to-algorithms-spring-2029/.

    • MIT 本科算法课,含网络流章节视频与习题。
  2. Roughgarden, T. 2024. CS261 Algorithms: Network Flows. Stanford University. Retrieved from https://web.stanford.edu/class/cs261/.

    • Stanford 高级算法课,深入网络流对偶理论。

11. 延伸阅读

网络流算法与众多算法、数据结构、优化理论存在深刻联系。本节推荐五个延伸方向,助力读者构建完整的算法知识体系。

11.1 关联算法

  • 最短路径算法:Dijkstra、Bellman-Ford、Floyd-Warshall。网络流的残量网络最短路径计算与最短路径算法紧密相关,Edmonds-Karp 本质是 BFS 最短路径的扩展。
  • 最小生成树:Kruskal、Prim。虽然属于无向算法,但与最小割问题存在对偶联系(例如 Karger 随机最小割算法)。
  • 强连通分量:Tarjan、Kosaraju。在网络流的残量网络分析中,SCC 用于识别”必经边”与”循环流”。
  • 拓扑排序:Dinic 算法的分层构建本质是有向无环的拓扑排序。

11.2 数据结构支撑

  • 线段树(Segment Tree):在动态网络流中,线段树用于维护边的容量区间更新。参见 FANDEX 线段树文档。
  • 并查集(Disjoint Set Union):最小割的 Karger 算法依赖并查集实现顶点合并。参见 FANDEX 并查集文档。
  • 斐波那契堆(Fibonacci Heap):Dijkstra 的 O(E+VlogV)O(E + V\log V) 实现依赖斐波那契堆,MCMF 的 Johnson 消负权也用到。
  • 动态树(Link-Cut Tree):网络单纯形的高效实现依赖动态树维护生成树。

11.3 优化理论

  • 线性规划(Linear Programming):最大流问题是 LP 的特例,LP 对偶理论直接推导最大流最小割定理。单纯形法与内点法均可求解网络流。
  • 凸优化(Convex Optimization):最小费用流是凸优化问题,可用梯度下降、牛顿法求解。
  • 拉格朗日松弛(Lagrangian Relaxation):多商品流问题通过拉格朗日松弛分解为单商品流。
  • 整数规划(Integer Programming):整数网络流是 NP-hard,需分支定界或割平面法。

11.4 算法理论知识点

参见 FANDEX 算法理论知识点文档,涵盖:

  • P vs NP:网络流是 P 问题,但整数多商品流是 NP-hard。
  • 近似算法:许多 NP-hard 流问题有 O(logn)O(\log n) 近似算法。
  • 在线算法:在线最大流问题研究流量动态到达时的竞争比。
  • 随机化算法:Karger 随机最小割算法是经典示例。

11.5 动态规划与网络流

参见 FANDEX 动态规划文档,二者联系包括:

  • 最短路 DP:DAG 最短路既是 DP 又是网络流特例。
  • 树形 DP:树形网络的最大流可由树形 DP 直接求解。
  • 区间 DP:某些网络流问题的特殊结构可由区间 DP 高效求解。

11.6 推荐论文与综述

  • Goldberg, A. V. 1998. Recent developments in maximum flow algorithms. In Algorithmica.
  • Schrijver, A. 2003. Combinatorial Optimization: Polyhedra and Efficiency. Springer.
  • Williamson, D. P. 2019. Network Flow Algorithms. Cambridge University Press.

11.7 实战训练

  • LeetCode:网络流专题(如 460. LFU 缓存、146. LRU 缓存等,虽非直接网络流但涉及容量调度)。
  • Codeforces:搜索 “flow” 标签,数百道网络流题目。
  • AtCoder:日本竞赛平台,网络流题目质量高。
  • USACO:美国信息学奥赛,含网络流教学章节。

结语

网络流算法是计算机科学中最优雅的理论成果之一,它将”流动”这一物理直觉转化为严谨的数学模型,并在工程实践中展现出惊人的普适性。从 Ford-Fulkerson 1956 年的开创性工作到 Orlin 2013 年的 O(VE)O(VE) 突破,网络流算法的发展史本身就是算法理论进步的缩影。

掌握网络流的关键在于:

  1. 理解建模思想:将现实问题归约至最大流、最小割、最小费用流。
  2. 熟练核心算法:Dinic 与 Push-Relabel 是工程主力。
  3. 洞察复杂度边界:不同算法在不同结构下的表现差异巨大。
  4. 积累应用案例:二分匹配、Project Selection、Image Segmentation 等是建模范本。

愿本文档成为读者探索网络流世界的可靠指南,从理论推导到工程落地,循序渐进,融会贯通。


最后审阅:2026-07-18 · FANDEX Content Engineering