网络流总结
定义
网络流,简单来讲就是一个有向无负环图,有一个源点,一个汇点。每条边上的边权就是这条边的最大流量。
一个值 \(x\),满足单位时间内源点灌入 \(x\) 单位的水的同时汇点会汇入 \(x\) 单位的水(也就是中间所有节点的流入等于流出,每条边流量不超过容量),这种情况下就是一个合法的网络流。
常见的网络流问题包括但不限于以下类型问题.
最大流问题:对于网络 \(G =(V, E)\),给每条边指定流量,得到合适的流 \(f\),使得 \(f\) 的流量尽可能大.此时我们称 \(f\) 是 \(G\) 的最大流.
最小割问题:对于网络 \(G =(V, E)\),找到合适的 \(s\)-\(t\) 割 \(\{S, T\}\),使得 \(\{S, T\}\) 的总容量尽可能小.此时我们称 \(\{S, T\}\) 的总容量是 \(G\) 的最小割.
最小费用最大流问题:在网络 \(G =(V, E)\) 上,对每条边给定一个权值 \(w(u, v)\),称为费用(cost),含义是单位流量通过 \((u, v)\) 所花费的代价.对于 \(G\) 所有可能的最大流,我们称其中总费用最小的一者为最小费用最大流.
我们先来分析最大流问题。
最大流问题
那你可能要问了,主播主播,我就不能直接从起点出发跑一个 DFS 吗?
看起来没什么问题,但是这样不一定是最优的,我们看一个例子:

最优路线为下:
S->A->T,流量为 1
S->B->T,流量为 1
S->A->B->T,流量为 1
总流量为 3
但是,如果是 DFS,我们可能出现以下的情况:
S->A->B->T,流量为 2
发现已经没法走了,总流量为 2
由此可见, 贪心的走并不能一定让总流量最大。
接下来我们引入一些定义
给定网络 \(G\) 及 \(G\) 上的流 \(f\),我们做如下定义.
对于边 \((u, v)\),我们将其容量与流量之差称为剩余容量 \(c_f(u, v)\),即 \(c_f(u, v) = c(u, v) - f(u, v)\),\(f(u, v)\)表示的是这个边单位时间还最多能多流过多少单位的水。
我们将 \(G\) 中所有结点和剩余容量大于 \(0\) 的边构成的子图称为残量网络 \(G_f\),即 \(G_f = (V, E_f)\),其中 \(E_f = \left\{ (u, v) \mid c_f(u, v) > 0 \right\}\),即现在把所有“还能灌水进去”的水管挑出来(不管水流方向),画成一张新地图。
我们将 \(G_f\) 上一条从源点 \(s\) 到汇点 \(t\) 的路径称为增广路.对于一条增广路,我们给每一条边 \((u, v)\) 都加上等量的流量,以令整个网络的流量增加,这一过程被称为增广.由此,最大流的求解可以被视为若干次增广分别得到的流的叠加。
此外,在 Ford–Fulkerson 增广的过程中,对于每条边 \((u, v)\),我们都新建一条反向边 \((v, u)\).我们约定 \(f(u, v) = -f(v, u)\),这一性质可以通过在每次增广时引入退流操作来保证,即 \(f(u, v)\) 增加时 \(f(v, u)\) 应当减少同等的量。
也就是对于原图里每条边 \(u \to v\),我们都凭空创造一条反向边 \(v \to u\)。
这条反向边的初始流量是 \(0\),容量也是 \(0\)(在原始输入里不存在)。但是剩余容量会随着算法运行而变化。
当你沿着增广路推送流量时,如果走的是:
正向边 \(u \to v\):
\(f(u, v)\) 增加,同时 \(f(v, u)\) 减少同等的量。
反向边 \(v \to u\)(即你走了一条"幻想"的反向路):
\(f(v, u)\) 增加,同时 \(f(u, v)\) 减少同等的量。
你可能问,这个反向边有什么用呢?
我们发现,我们可以将反向边流量的减少视为反向边剩余容量 \(c_f(v, u)\) 的增加——这也与退流的意义相吻合——反向边剩余容量的增加意味着我们接下来可能通过走反向边来和原先正向的增广抵消,代表一种「反悔」的操作。
我们看图:

\(G\) 上有多条增广路,其中,我们选择进行一次先后经过 \(u, v\) 的增广(如左图所示),流量增加 \(1\)。
我们注意到,如果进行中图上的增广,这个局部的最大流量不是 \(1\) 而是 \(2\).但由于指向 \(u\) 的边和从 \(v\) 出发的边在第一次增广中耗尽了容量,此时我们无法进行中图上的增广.这意味着我们当前的流是不够优的,但局部可能已经没有其他(只经过原图中的边而不经过反向边的)增广路了。
现在引入退流操作.第一次增广后,退流意味着 \(c_f(v, u)\) 增加了 \(1\) 剩余容量,即相当于新增 \((v, u)\) 这条边,因此我们可以再进行一次先后经过 \(p, v, u, q\) 的增广(如右图橙色路径所示).无向边 \((u, v)\) 上的流量在两次增广中抵消,我们惊奇地发现两次增广叠加得到的结果实际上和中图是等价的。
所以,我们可以认为建反向边就是代表一种「反悔」的操作。
以上案例告诉我们,退流操作带来的「抵消」效果使得我们无需担心我们按照「错误」的顺序选择了增广路。
容易发现,只要 \(G_f\) 上存在增广路,那么对其增广就可以令总流量增加;否则说明总流量已经达到最大可能值,求解过程完成.这就是 Ford–Fulkerson 增广的过程。
正确性的证明,就是使用“最大流最小割定理”,具体的证明此处从省,仅给出结论。
最大流最小割定理
- 割:将顶点集 \(V\) 分为两个集合 \(S\) 和 \(T\),其中 \(s \in S, t \in T\)。
- 割集里的边,必须是从 S 能到达的一侧,指向 T 能到达的一侧。
- 割的容量:\(c(S,T) = \sum_{u \in S} \sum_{v \in T} c(u,v)\),即所有从 \(S\) 跨到 \(T\) 的边的容量和。
- 割的净流量:\(f(S,T) = \sum_{u \in S} \sum_{v \in T} f(u,v) - \sum_{u \in S} \sum_{v \in T} f(v,u)\)。
- 最小割可以理解为:让源点 S 和汇点 T 不连通,并且割掉的边的总容量最小。
- 在任何网络中,最大流的值等于最小割的容量。
Edmonds-Karp 算法
Ford-Fulkerson 方法本身并没有规定如何寻找增广路。如果随便找,复杂度可能无法保证(甚至可能因为实数容量而陷入无限循环,但在整数容量下会终止)。
Edmonds-Karp (EK) 算法是对 Ford-Fulkerson 的一个经典改进:每次找一条从源点到汇点的边数最少的增广路(也就是在残量网络上跑 BFS)。
算法步骤
-
初始化所有边的流量为 0。
-
在残量网络 \(G_f\) 上,用 BFS 寻找从 \(s\) 到 \(t\) 的最短路径(按边数)。
-
如果找不到路径,算法终止,当前流就是最大流。
-
如果找到了路径 \(P\),计算路径上的最小剩余容量 \(\Delta = \min_{(u,v) \in P} c_f(u,v)\)
-
沿着路径 \(P\) 增广流量 \(\Delta\):
- 对每条正向边 \((u,v)\): \(f(u,v) \mathrel{+}= \Delta\)
- 对每条反向边 \((v,u)\): \(f(v,u) \mathrel{-}= \Delta\) (即剩余容量增加 \(\Delta\))
-
回到第 2 步。
EK 每次选最短增广路,可以保证增广次数不会太多。每条边成为“瓶颈边”(即某次增广中剩余容量最小的边)的次数是有限的,整体复杂度为
\(O(VE^2)\)
#include<bits/stdc++.h>
using namespace std;
#define ll long long
ll n,m,s,t;
struct nood{
ll nxt,v,c;
}e[1000000];
ll head[1000000],tot=1;
ll pree[1000000],preu[1000000];// pree[v]: 到达v的边编号, preu[v]: 到达v的前驱点
ll vis[1000000];
void adde(ll x,ll y,ll z){
e[++tot].v=y,e[tot].c=z,e[tot].nxt=head[x],head[x]=tot;
e[++tot].v=x,e[tot].c=0,e[tot].nxt=head[y],head[y]=tot;
}
bool bfs(){
memset(vis,0,sizeof(vis));
queue<ll>q;
q.push(s);
vis[s]=1;
while(!q.empty()){
ll x=q.front();
q.pop();
for(int i=head[x];i;i=e[i].nxt){
ll v=e[i].v;
if(!vis[v]&&e[i].c>0){
vis[v]=1;
pree[v]=i;
preu[v]=x;
if(v==t){
return 1;
}
q.push(v);
}
}
}
return 0;
}
ll EK(){
ll ans=0;
while(bfs()){
ll d=1e18;
for(int v=t;v!=s;v=preu[v]){
d=min(d,e[pree[v]].c);
}
for(int v=t;v!=s;v=preu[v]){
e[pree[v]].c-=d;
e[pree[v]^1].c+=d;
}
ans+=d;
}
return ans;
}
int main(){
cin>>n>>m>>s>>t;
ll x,y,z;
for(int i=1;i<=m;i++){
cin>>x>>y>>z;
adde(x,y,z);
}
cout<<EK();
}
Dinic 算法详解
核心思路
Dinic 在 EK 的基础上做了两大优化:
-
分层图:每次 BFS 给所有点标深度 \(level[v]\)(从 \(s\) 出发的最短边数)。只保留 \(level[to] == level[from] + 1\) 的边进行增广。这样保证每次 DFS 走最短路,避免绕圈。
-
多路增广:在分层图上用 DFS 一次性找到尽可能多的增广路(阻塞流),而不是像 EK 那样每次只找一条。
分层图的作用
-
确保 DFS 不会走回头路(因为深度严格递增)。
-
每次增广后,分层图一定“变差”(\(s\) 到 \(t\) 的最短距离增加),所以最多重建 \(O(V)\) 次分层图。
DFS 增广过程
\(dfs(u, t, f)\) 表示当前在点 \(u\),要从 \(u\) 往 \(t\) 推 \(f\) 单位流量,返回实际推动了多少。
-
如果 \(u == t\),直接返回 \(f\)。
-
遍历 \(u\) 的所有出边,只走 \(level[v] == level[u] + 1\) 且剩余容量 \(>0\) 的边。
-
对每条可走的边,递归调用 \(dfs(v, t, \min(f, e.cap))\)。
-
如果某条边无法再推流(返回 0),就换下一条。
-
当前弧优化:用 \(iter[u]\) 记录这次 DFS 中 \(u\) 已经尝试到第几条边了,下次直接从这条继续,避免重复检查已经满的边。
复杂度
-
每次构建分层图 \(O(E)\)。
-
每次 DFS 找阻塞流 \(O(VE)\)(有当前弧优化时)。
-
最多重建 \(O(V)\) 次分层图。
-
总复杂度 \(O(V^2E)\)。实际远快于此。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
ll n,m,s,t;
struct nood{
ll nxt,v,c;
}e[1000000];
ll head[1000000],tot=1;
ll cur[1000000]; // 当前弧优化
ll dep[1000000]; // 分层深度
ll vis[1000000];
queue<ll> q;
void adde(ll x,ll y,ll z){
e[++tot].v=y,e[tot].c=z,e[tot].nxt=head[x],head[x]=tot;
e[++tot].v=x,e[tot].c=0,e[tot].nxt=head[y],head[y]=tot;
}
bool bfs(){
memset(dep,0,sizeof(dep));
while(!q.empty()){
q.pop();
}
q.push(s);
dep[s]=1;
while(!q.empty()){
ll x=q.front();
q.pop();
for(int i=head[x];i;i=e[i].nxt){
ll v=e[i].v;
if(!dep[v]&&e[i].c>0){
dep[v]=dep[x]+1;
q.push(v);
if(v==t){
return 1;
}
}
}
}
return 0;
}
ll dfs(ll x,ll f){
if(x==t||f==0){
return f;
}
ll ls=0;
for(int i=cur[x];i;i=e[i].nxt){
cur[x]=i;
ll v=e[i].v;
if(dep[v]==dep[x]+1&&e[i].c>0){
ll d=dfs(v,min(f-ls,e[i].c));
if(d>0){
e[i].c-=d;
e[i^1].c+=d;
ls+=d;
if(ls==f){
break;
}
}
}
}
return ls;
}
ll Dinic(){
ll ans=0;
while(bfs()){
for(int i=1;i<=n;i++){
cur[i]=head[i];
}
ll f;
while(1){
f=dfs(s,1e18);
if(f>0){
ans+=f;
}
else{
break;
}
}
}
return ans;
}
int main(){
cin>>n>>m>>s>>t;
ll x,y,z;
for(int i=1;i<=m;i++){
cin>>x>>y>>z;
adde(x,y,z);
}
cout<<Dinic();
}
最小费用最大流
问题定义
在最大流的基础上,每条边除了容量 cap,还有一个单位费用 cost,表示每单位流量经过这条边要花多少钱。
要求:在流量最大的前提下,总费用最小。
核心思路
回想 Dinic 是每次 BFS 找最短增广路(边数最少),然后增广。
MCMF 的思路类似,但把"最短"改成"最便宜":
每次在残量网络上找一条从 s 到 t 的费用最小的增广路,然后沿这条路增广。
因为反向边用于退流,所以反向边的费用是正向边的相反数(退流等于退钱)。
算法:SSP
这是最基础的 MCMF 算法,本质就是把 EK 的 BFS 换成 SPFA(或 Dijkstra)按费用找最短路。
算法步骤
-
初始化流量为 0。
-
在残量网络上,以费用为边权,跑
SPFA找从 s 到 t 的最短路。 -
如果找不到路径(不可达),算法结束。
-
沿最短路增广,流量增加
d = min(cap),费用增加d × dist[t]。
重复步骤 2-4。
为什么贪心是对的?
因为每次选的是当前最便宜的增广路,且反向边允许反悔。在流量递增的过程中,费用也是递增的,最终得到的就是最小费用最大流。
复杂度
设最大流为 F,每次 SPFA 是 \(O(VE)\),最多增广 F 次(如果每次只推 1 单位)。
总复杂度 \(O(F × VE)\),在流量不大时够用。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
ll n,m,s,t;
struct nood{
ll nxt,v,c,w;
}e[1000000];
ll head[1000000],tot=1,dis[1000000];
ll pree[1000000],preu[1000000];// pree[v]: 到达v的边编号, preu[v]: 到达v的前驱点
ll vis[1000000],ans1;
void adde(ll x,ll y,ll z,ll w){
e[++tot].v=y,e[tot].c=z,e[tot].w=w,e[tot].nxt=head[x],head[x]=tot;
e[++tot].v=x,e[tot].c=0,e[tot].w=-w,e[tot].nxt=head[y],head[y]=tot;
}
bool spfa(){
for(int i=0;i<1000000;i++){
dis[i]=1e9;
}
memset(vis,0,sizeof(vis));
queue<ll>q;
while(!q.empty()){
q.pop();
}
q.push(s);
dis[s]=0;
vis[s]=1;
while(!q.empty()){
ll x=q.front();
q.pop();
vis[x]=0;
for(int i=head[x];i;i=e[i].nxt){
ll v=e[i].v;
if(e[i].c>0&&dis[v]>dis[x]+e[i].w){
dis[v]=dis[x]+e[i].w;
pree[v]=i;
preu[v]=x;
if(!vis[v]){
vis[v]=1;
q.push(v);
}
}
}
}
return dis[t]<=1e8;
}
ll MCMF(){
ll ans=0;
ll mincost=0;
while(spfa()){
ll d=1e18;
for(int v=t;v!=s;v=preu[v]){
d=min(d,e[pree[v]].c);
}
for(int v=t;v!=s;v=preu[v]){
e[pree[v]].c-=d;
e[pree[v]^1].c+=d;
}
ans+=d;
mincost+=d*dis[t];
}
ans1=mincost;
return ans;
}
int main(){
cin>>n>>m>>s>>t;
ll x,y,z,w;
for(int i=1;i<=m;i++){
cin>>x>>y>>z>>w;
adde(x,y,z,w);
}
cout<<MCMF();
cout<<" "<<ans1;
return 0;
}
本文来自博客园,作者:MistyPost,转载请注明原文链接。

浙公网安备 33010602011771号