跳转到内容

2026-08-21 网络流

某个国家有 nn 个城市,相邻城市之间共有 mm 条高速公路。每辆车经过一条高速公路都要 11 小时;路宽不同,所以同一时刻各条路能容纳的车数也不同。高速公路可以双向开,但两个方向各有各的容量,互不占用。

无数辆车要从同一个起点城市开到同一个终点城市,中途不允许停留。问:同一时刻最多能发出多少辆车?

下图是一个具体例子。

带容量的流网络

  • 共有四个城市 s,a,b,ts,a,b,t。ss 是起点,tt 是终点,所有车都从 ss 开到 tt。
  • 边上的数字是该方向同一时刻的容量。例如 s→as\to a 最多同时容纳 11 辆车。
  • 每个小时发一次车,每条路都走 11 小时,中途城市不许停车。问每个小时最多能发多少辆。

同一张路上,调度方案不同,每个小时能发出的车数也可以不同。下图左边和右边是两种都合法的方案;绿色是这一小时有车在走的路,灰色是没走的路。边上写成「正在开的车 / 容量」。

两种派车方案

方案一走中间的 a→ba\to b,占住 s→as\to a 和 b→tb\to t,上下直达走不成,每小时只能发 11 辆。方案二走 s→a→ts\to a\to t 与 s→b→ts\to b\to t 两条不相交的路,每小时发 22 辆。

把上面的发车问题写成数学对象,就是流网络,记为有向图 G=(V,E)G=(V,E):点是城市,边是公路的一个方向。双向公路写成两条边,去和回各算各的。指定起点 ss、终点 tt。

每条边有非负容量 c(e)c(e):该方向同一时刻最多能容纳多少辆车。

流 ff 是一种调度方案,f(e)f(e) 是这个方案里沿边 ee 走的车数。合法方案要满足两条约束。

① 容量约束:路上的车不能是负数,也不能超过该方向的容量。

0≤f(e)≤c(e)0\le f(e)\le c(e)

② 流量守恒:除 ss、tt 外,开进一个城市的车必须全部开出,中途不许停车。

∑u:(u,v)∈Ef(u,v)=∑w:(v,w)∈Ef(v,w),v∈V∖{s,t}\sum_{u:(u,v)\in E}f(u,v) = \sum_{w:(v,w)\in E}f(v,w), \qquad v\in V\setminus\{s,t\}

流值 ∣f∣|f| 是离开起点 ss 的净车数,也就是这个方案这一小时发出了多少辆;它等于到达终点 tt 的净车数。

∣f∣=∑w:(s,w)∈Ef(s,w)−∑u:(u,s)∈Ef(u,s)|f| = \sum_{w:(s,w)\in E}f(s,w) - \sum_{u:(u,s)\in E}f(u,s)

最大流就是使 ∣f∣|f| 最大的那种调度方案。

先选择一种调度方案:沿 s→a→b→ts\to a\to b\to t 派出 11 单位流量。这是一个合法方案。

随机派出一辆后再往 b 派一辆走不通

接着尝试沿 s→bs\to b 继续派流量。流量到达 bb 后,发现 b→tb\to t 的容量已经被原方案占满,无法继续到达 tt。

这个例子很简单:把原来的路线改成 s→a→ts\to a\to t,再让新流量沿 s→b→ts\to b\to t 发送,就能把流值从 11 增加到 22。

但是,当流网络变得复杂后怎么办?如何改变之前的调度方案,使它适配新的流量?算法如何终止?

先分析一种特殊情况。本节用 u→vu\to v 表示相邻节点之间的一条边,用 u⇝vu\rightsquigarrow v 表示一段可能经过多个中间节点的曲线路径。

当前有 xx 单位流量沿 s⇝a→b⇝ts\rightsquigarrow a\to b\rightsquigarrow t 发送。把这条旧流量反过来写成虚线,就表示对应部分最多可以撤销 xx。反向虚线只是修改方案的余地,不表示车辆真的倒着开。

再把普通实线表示的剩余容量接起来。如果能拼出

s⇝c→b→a⇝t,s\rightsquigarrow c\to b\to a\rightsquigarrow t,

就得到了一条新的调整路径。其中 b→ab\to a 走反向虚线,其余部分走剩余容量。这条路径最终能够增加从 ss 到 tt 的流量,因此叫作增广路。

把旧流量反向表示,并拼出新的调整路径

设整条调整路径最多还能通过 yy,其中 y≤xy\le x。接下来经过两次实际修改,再把新流量反向记录。

第一步,把原来沿 a→b⇝ta\to b\rightsquigarrow t 发送的 yy 改走曲线 a⇝ta\rightsquigarrow t。a→ba\to b 与 b⇝tb\rightsquigarrow t 都释放出 yy 的剩余容量,总流值仍是 xx。

第二步,沿 s⇝c→b⇝ts\rightsquigarrow c\to b\rightsquigarrow t 引入红色的新流量 yy。它会用掉 b⇝tb\rightsquigarrow t 刚释放的容量,但 a→ba\to b 上释放的 yy 仍然保留。最终流值变为 x+yx+y。最后把新流量也反向记录,供后续修改。

找到调整路径、重路由、引入新流量并反向记录

这整个过程叫作增广。yy 由整条调整路径最窄的位置决定:普通实线受剩余容量限制,反向虚线受可撤销量限制。

所有边的增减同时发生,所以每个中间节点仍满足流量守恒。

对原边 u→vu\to v,当前流量为 f(u,v)f(u,v) 时,正向剩余容量是 c(u,v)−f(u,v)c(u,v)-f(u,v),反向可撤销量是 f(u,v)f(u,v)。

沿增广路增加 yy 时,路径上每条边的可用量减去 yy,反向边的可用量增加 yy;下图红色部分就是把 s⇝c→b→a⇝ts\rightsquigarrow c\to b\to a\rightsquigarrow t 上的 yy 取反。

增广前后的路径方向对比

Ford–Fulkerson 不规定怎样寻找增广路。给原图中的每条边 ee 建立一条独立的配对反向边 rev⁡(e)\operatorname{rev}(e),用 r(e)r(e) 表示当前可用量:

for 原图中的每条边 e:
r(e) = c(e)
r(rev(e)) = 0
flow = 0
while 能找到一条从 s 到 t 的增广路 P:
Δ = min{r(e) | e 在 P 上}
for P 上的每条边 e:
r(e) -= Δ
r(rev(e)) += Δ
flow += Δ
return flow

每轮都会使总流值增加 Δ\Delta:中间节点同时增加一份流入和流出,只有 ss 的净流出与 tt 的净流入增加 Δ\Delta。

对每条原边 ee,配对反向边中的可用量记录了原边上的流量;把所有反向记录翻回原边方向,就能逐边还原一份流值相同的流。这里的一一对应发生在配对边之间,流量路径的分解不一定唯一。

Ford–Fulkerson 不断寻找增广路,直到再也找不到。算法停止只说明当前流无法继续调整,还不能直接说明它已经是最大流;要证明这一点,先从一种划分节点的方法——割——开始。

一个 ss-tt 割是对全部节点的划分 (S,T)(S,T),满足 S∪T=VS\cup T=V、S∩T=∅S\cap T=\varnothing、s∈Ss\in S、t∈Tt\in T。割本身是节点的划分,不是一组被删除的边。

从 SS 指向 TT 的边叫作割边。割的容量是这些边的容量之和:

c(S,T)=∑u∈S, v∈Tc(u,v).c(S,T)=\sum_{u\in S,\,v\in T}c(u,v).

方向不能忽略:从 TT 指向 SS 的边不计入 c(S,T)c(S,T)。同一张网络可以按照多种方式划分节点,因此可以产生多个不同的割。

一个流穿过割的净流量,等于从 SS 流向 TT 的流量减去从 TT 流回 SS 的流量:

f(S,T)=∑u∈S, v∈Tf(u,v)−∑u∈T, v∈Sf(u,v).f(S,T) = \sum_{u\in S,\,v\in T}f(u,v) - \sum_{u\in T,\,v\in S}f(u,v).

割容量只计算正向边的容量,割净流量则要减去反向流量。

同一张网络的三种割

图中三种划分的割容量分别是 66、88 和 77。在所有割中,容量最小的割叫作最小割。

给定流网络 G=(V,E,c)G=(V,E,c) 与可行流 ff。对每条原边 e=(u,v)∈Ee=(u,v)\in E,用 eˉ=(v,u)\bar e=(v,u) 表示与它配对的反向边,并记

Eˉ={eˉ∣e∈E}.\bar E=\{\bar e\mid e\in E\}.

原边及其配对反向边的残量容量定义为

cf(e)=c(e)−f(e),cf(eˉ)=f(e).c_f(e)=c(e)-f(e), \qquad c_f(\bar e)=f(e).

只保留残量容量大于 00 的边:

Ef={a∈E∪Eˉ∣cf(a)>0}.E_f=\{a\in E\cup\bar E\mid c_f(a)>0\}.

由此得到可行流 ff 对应的残量网络:

Gf=(V,Ef,cf).G_f=(V,E_f,c_f).

图中灰线表示原方向的残量容量,红线表示可行流;得到 GfG_f 时,灰线方向不变,红线方向反转。

流 f 与对应的残量网络 G_f

反向边 eˉ\bar e 只是原边 ee 的配对边,不要求它存在于原图中。如果原图本来就有一条 v→uv\to u,它与 eˉ\bar e 仍是两条不同的边。

最大流最小割定理指出:最大流的流值等于最小割的容量。

max⁡f∣f∣=min⁡(S,T)c(S,T).\max_f |f|=\min_{(S,T)}c(S,T).

证明只需要两步:先说明任意割都是任意流的上界,再说明没有增广路时,可以构造一组流值与割容量相等的流和割。

第一步:任意流的流值不超过任意割的容量。

先证明任意割的净流量都等于整个网络的流值。定义节点 uu 的净流出量:

net⁡f(u)=∑v∈Vf(u,v)−∑v∈Vf(v,u).\operatorname{net}_f(u) = \sum_{v\in V}f(u,v) - \sum_{v\in V}f(v,u).

因为 (S,T)(S,T) 是一个 ss-tt 割,所以 s∈Ss\in S、t∈Tt\in T。S∖{s}S\setminus\{s\} 中的节点都是中间节点,净流出量均为 00;只有源点满足 net⁡f(s)=∣f∣\operatorname{net}_f(s)=|f|。因此

∣f∣=∑u∈Snet⁡f(u)=∑u∈S∑v∈Vf(u,v)−∑u∈S∑v∈Vf(v,u)=∑u∈S∑v∈Sf(u,v)+∑u∈S∑v∈Tf(u,v)−∑u∈S∑v∈Sf(v,u)−∑u∈S∑v∈Tf(v,u).\begin{aligned} |f| &=\sum_{u\in S}\operatorname{net}_f(u)\\ &=\sum_{u\in S}\sum_{v\in V}f(u,v) -\sum_{u\in S}\sum_{v\in V}f(v,u)\\ &=\sum_{u\in S}\sum_{v\in S}f(u,v) +\sum_{u\in S}\sum_{v\in T}f(u,v)\\ &\quad-\sum_{u\in S}\sum_{v\in S}f(v,u) -\sum_{u\in S}\sum_{v\in T}f(v,u). \end{aligned}

前后两个 SS 内部的求和只交换了变量名,因此完全相等并相互抵消。剩下的正是割的净流量:

∣f∣=∑u∈S∑v∈Tf(u,v)−∑u∈S∑v∈Tf(v,u)=f(S,T).\begin{aligned} |f| &=\sum_{u\in S}\sum_{v\in T}f(u,v) -\sum_{u\in S}\sum_{v\in T}f(v,u)\\ &=f(S,T). \end{aligned}

再证明割的净流量不超过割容量。反向流量非负,正向流量不超过对应边的容量,因此

f(S,T)=∑u∈S, v∈Tf(u,v)−∑u∈T, v∈Sf(u,v)≤∑u∈S, v∈Tf(u,v)≤∑u∈S, v∈Tc(u,v)=c(S,T).\begin{aligned} f(S,T) &=\sum_{u\in S,\,v\in T}f(u,v) -\sum_{u\in T,\,v\in S}f(u,v)\\ &\le \sum_{u\in S,\,v\in T}f(u,v)\\ &\le \sum_{u\in S,\,v\in T}c(u,v)\\ &=c(S,T). \end{aligned}

结合两部分,对任意可行流 ff 和任意割 (S,T)(S,T) 都有

∣f∣≤c(S,T).|f|\le c(S,T).

第二步:没有增广路时,构造一个与流值相等的割。

设可行流 ff 对应的残量网络为 GfG_f,并且 GfG_f 中找不到从 ss 到 tt 的增广路。令 SS 为 GfG_f 中从 ss 可达的全部节点,令 T=V−ST=V-S。因为 tt 不可达,所以 s∈Ss\in S、t∈Tt\in T,(S,T)(S,T) 构成一个割。

从没有增广路的残量网络构造等值割

对任意原边 u→vu\to v,其中 u∈Su\in S、v∈Tv\in T。如果 cf(u,v)>0c_f(u,v)>0,那么 vv 也能从 ss 到达,与 v∈Tv\in T 矛盾。因此

cf(u,v)=c(u,v)−f(u,v)=0,c_f(u,v)=c(u,v)-f(u,v)=0,

所以

f(u,v)=c(u,v),f(u,v)=c(u,v),

即所有从 SS 指向 TT 的原边都已满流。

再考虑任意原边 u→vu\to v,其中 u∈Tu\in T、v∈Sv\in S。它配对的反向边 v→uv\to u 从 SS 指向 TT,同理,其残量容量必须为 00,因此

cf(v,u)=f(u,v)=0.c_f(v,u)=f(u,v)=0.

所以从 SS 指向 TT 的流量等于对应边的容量,从 TT 指向 SS 的流量为 00:

∣f∣=f(S,T)=c(S,T).|f|=f(S,T)=c(S,T).

第一步说明任意流都不超过任意割;现在存在一组流和割恰好相等。因此这个流是最大流,这个割是最小割,并且最大流值等于最小割容量。

只要找到一个可行流 ff,并且它对应的残量网络 GfG_f 中不存在从 ss 到 tt 的增广路,那么 ff 就是最大流。

Ford–Fulkerson 不规定如何寻找增广路。如果容量为整数,一种糟糕的选路方式可能每次只增加 11 单位流量,使复杂度达到 O(EF)O(EF),其中 FF 是最大流值。

Edmonds–Karp 固定使用 BFS,每次选择残量网络中边数最少的增广路。增广方式没有变化,变化的只是选路规则。

在残量网络 GfG_f 中,定义 df(v)d_f(v) 为从 ss 到 vv 最少需要经过的边数。BFS 按照距离递增的顺序访问节点,因此第一次到达 tt 时,得到的就是一条最短增广路。

Edmonds–Karp 使用 BFS 选择边数最少的增广路

这里的“最短”只表示经过的边最少,不表示剩余容量最大。找到路径后,仍然取路径上的最小残量容量 Δ\Delta,再完成一次普通增广。

4.2 为什么复杂度与最大流值无关

Section titled “4.2 为什么复杂度与最大流值无关”

关键性质是:每次增广之后,从 ss 到任意节点的最短距离都不会减小。

设增广前后的距离分别为 d(v)d(v) 和 d′(v)d'(v)。假设某个节点满足 d′(v)<d(v)d'(v)<d(v),并选择新距离最小的这种节点。令 uu 是新最短路上 vv 的前驱,则

d′(u)=d′(v)−1,d′(u)≥d(u).d'(u)=d'(v)-1, \qquad d'(u)\ge d(u).

如果增广前已经存在残量边 u→vu\to v,就有

d(v)≤d(u)+1≤d′(u)+1=d′(v),d(v)\le d(u)+1\le d'(u)+1=d'(v),

与 d′(v)<d(v)d'(v)<d(v) 矛盾。因此 u→vu\to v 只能是本次增广新产生的反向边,也就是旧的最短增广路经过了 v→uv\to u。旧距离满足

d(u)=d(v)+1.d(u)=d(v)+1.

于是

d′(v)=d′(u)+1≥d(u)+1=d(v)+2,d'(v)=d'(u)+1\ge d(u)+1=d(v)+2,

仍然矛盾。因此所有节点的最短距离都不会减小。

每次增广至少会用满一条残量边。若同一条边 u→vu\to v 以后再次被用满,中间必须先沿反向边 v→uv\to u 增广,使 u→vu\to v 恢复残量容量。前后两次用满之间,uu 的距离至少增加 22。

节点距离最大为 V−1V-1,所以每条边最多被反复用满 O(V)O(V) 次。共有 O(E)O(E) 条边,因此增广次数为 O(VE)O(VE);每次 BFS 和更新路径需要 O(E)O(E),总复杂度为

O(VE2).O(VE^2).

这个上界只由图的节点数和边数决定,与最大流值 FF 无关。

flow = 0
while BFS(G_f, s, t) 能找到增广路:
用 parent[] 从 t 还原到 s
Δ = 路径上的最小残量容量
for 路径上的每条边 u → v:
c_f(u, v) -= Δ
c_f(v, u) += Δ
flow += Δ
return flow

Edmonds–Karp 每找到一条增广路,就重新执行一次 BFS。但一次 BFS 已经得到整张图的距离信息,只使用其中一条路径会浪费这些信息。

Dinic 的做法是:先用 BFS 建立分层图,再用 DFS 在分层图中不断增广,直到本轮所有最短增广路都被阻塞。

BFS 计算每个节点的层数:

level⁡(v)=df(v).\operatorname{level}(v)=d_f(v).

此时只是给节点标记层数,残量网络中的边还全部保留。

BFS 标记层次后的残量网络

只保留满足下面两个条件的残量边 u→vu\to v:

cf(u,v)>0,level⁡(v)=level⁡(u)+1.c_f(u,v)>0, \qquad \operatorname{level}(v)=\operatorname{level}(u)+1.

删除同层边和反向边后得到分层图

第二张图中的虚线只用来标记被删除的边,它们不属于分层图,DFS 不能经过。DFS 只能沿着层数增加 11 的边前进;同层边和指向更低层的反向边都会被删除。

保留下来的边组成分层图。边只会从第 ii 层指向第 i+1i+1 层,因此分层图中没有环;其中每条 ss 到 tt 的路径都是当前残量网络中的最短增广路。

Dinic 不在分层图中只找一条路径,而是用 DFS 尽量向前推流,直到每条 ss 到 tt 的路径上都至少有一条边被用满。此时得到的流叫作阻塞流。

Dinic 从残量网络建立分层图并寻找阻塞流

这里的 DFS 与 Ford–Fulkerson 中任意寻找增广路的 DFS 不同:它只能沿着层数增加 11 的边前进。BFS 控制全局方向,DFS 只负责在当前分层图中一次推送尽可能多的流量。

实现时通常使用当前弧优化。cur⁡(u)\operatorname{cur}(u) 记录节点 uu 下一条尚未处理的边;一条边已经用满或已经无法继续推流后,本轮不再从头检查它。

下面的 cap 始终表示当前残量容量;每条正向边都配有一条初始容量为 00 的反向边。

#include <algorithm>
#include <limits>
#include <queue>
#include <vector>
struct Dinic {
using i64 = long long;
struct Edge {
int to;
int rev;
i64 cap;
};
std::vector<std::vector<Edge>> graph;
std::vector<int> level;
std::vector<int> cur;
explicit Dinic(int n) : graph(n), level(n), cur(n) {}
void addEdge(int u, int v, i64 cap) {
Edge forward{v, static_cast<int>(graph[v].size()), cap};
Edge reverse{u, static_cast<int>(graph[u].size()), 0};
graph[u].push_back(forward);
graph[v].push_back(reverse);
}
bool bfs(int s, int t) {
std::fill(level.begin(), level.end(), -1);
std::queue<int> queue;
level[s] = 0;
queue.push(s);
while (!queue.empty()) {
int u = queue.front();
queue.pop();
for (const Edge& edge : graph[u]) {
if (edge.cap > 0 && level[edge.to] == -1) {
level[edge.to] = level[u] + 1;
queue.push(edge.to);
}
}
}
return level[t] != -1;
}
i64 dfs(int u, int t, i64 limit) {
if (u == t) {
return limit;
}
for (int& i = cur[u]; i < static_cast<int>(graph[u].size()); ++i) {
Edge& edge = graph[u][i];
if (edge.cap == 0 || level[edge.to] != level[u] + 1) {
continue;
}
i64 pushed = dfs(edge.to, t, std::min(limit, edge.cap));
if (pushed == 0) {
continue;
}
edge.cap -= pushed;
graph[edge.to][edge.rev].cap += pushed;
return pushed;
}
return 0;
}
i64 maxFlow(int s, int t) {
i64 flow = 0;
const i64 inf = std::numeric_limits<i64>::max();
while (bfs(s, t)) {
std::fill(cur.begin(), cur.end(), 0);
while (i64 pushed = dfs(s, t, inf)) {
flow += pushed;
}
}
return flow;
}
};

一轮结束后,当前分层图中不再存在 ss 到 tt 的路径。重新执行 BFS 时,tt 的层数必然严格增加,所以 BFS 最多执行 V−1V-1 轮。

使用当前弧优化后,一轮阻塞流的复杂度为 O(VE)O(VE);乘上最多 O(V)O(V) 轮,Dinic 在一般图上的时间复杂度为

O(V2E).O(V^2E).

三种方法的核心区别可以归结为如何使用增广路:

方法选路方式一次 BFS 后做什么一般复杂度
Ford–Fulkerson任意增广路不固定使用 BFSO(EF)O(EF)(整数容量)
Edmonds–KarpBFS 找一条最短增广路增广一次后重新 BFSO(VE2)O(VE^2)
DinicBFS 建立分层图DFS 求出阻塞流O(V2E)O(V^2E)