网络流是最大流,最小割,费用流,上下界网络流等在网络图上的问题的总称。
注意:文章的目的是让零基础的人初步了解网络流,因此存在一些被省略的证明以及不太严谨的描述,想进阶的可以看 OI-Wiki 或者看些其他巨佬的博客。本文也主要参考 OI-Wiki 对应部分完成。
1. 定义
-
网络:特殊的有向图,特殊之处在于有源点(\(s\))和汇点(\(t\)),每条边都定义了一个叫容量的权值。如图就是一个网络:

- 下文我们将有向边 \((u,v)\) 的容量记为 \(c(u,v)\)。若有向边不存在则值为 \(0\)。
-
流:
-
严谨定义:一个从边集到整数集(实数集)的函数 \(f(u,v)\),满足:
-
容量限制:每条边的流量不得超过其容量,即 \(0\le f(u,v)\le c(u,v)\);
-
流守恒性:除源节点和汇节点以外,流入一个节点的流量等于流出的流量。定义一个节点的净流量 \(f(u)=\sum_{v\in V}f(u,v)-\sum_{v\in V}f(v,u)\),即对于任意非源汇节点外的节点 \(u\),满足 \(f(u)=0\)。
-
-
通俗定义:将有向边看作管道,容量看作管子每一刻能流过的最大水量,节点看作管道的连接处,那么从源点流入这个系统的水需要满足:管子不能爆炸(不能通过比管道最大流量还大的流量),每一刻有多少水流入连接处就有多少流出去(连接处没有储水功能)。
-
流量:定义流 \(f\) 的大小 \(|f|\) 为 \(f(S)\)(等价于 \(-f(T)\))。通俗地讲,在管道不爆炸的前提下,你往源点灌了多少水。
-
最大流问题:求一个网络的最大流量,即你最多能往源点灌多少水。
-
-
割:
-
定义:将点集分为两个集合 \(S\),\(T\) 满足 \(s\in S\),\(t\in T\),则称 \(\{S,T\}\) 为网络的一个 \(s\)-\(t\) 割。
-
割的容量:一个 \(s\)-\(t\) 割的容量是所有起点在 \(S\) 集合,终点在 \(T\) 集合的有向边的容量(\(\sum_{u\in S}\sum_{v\in T}c(u,v)\))。注意:起点在 \(T\),终点在 \(S\) 的边并不算入容量。
-
最小割问题:求一个网络中容量最小的 \(s\)-\(t\) 割的容量。
-
2. 最大流问题
Ford–Fulkerson 增广
定义一条边的剩余容量为这条边的容量与流量的差(这个值代表这根管子你还能压榨多少流量)。则所有还有剩余流量的边组成了一个残量网络。
容易想到一个贪心:从源点开始在残量网络上 DFS,如果能 DFS 到汇点,代表在残量网络上找到了一条从源点到汇点的路径(这样的路径被称为增广路),此时我们可以给这条路径上的每条边增流,增的量为路径上最少的剩余流量。找不到就结束。
比如我们在残量网络上找到了增广路 \(s\xrightarrow{3}u\xrightarrow{5}v\xrightarrow{2}t\)(箭头上面的数字表示剩余容量),那么我们可以让这条增广路上的所有边多 \(2\) 的流量,同时剩余流量减少 \(2\),这样在不超过每条边的容量的情况下增流最多。
然而这么做肯定是不对的,反例大把大把地有。贪心不对,就考虑反悔贪心。具体地,我们给每条边 \((u,v)\) 建立一条反向边 \((v,u)\),这条边的容量为 \(0\),流量为 \(-f(u,v)\),这时这条边的剩余容量负负得正就是 \(f(u,v)\),表示正向边能反悔多少流量。当我们给这两条边其中一条增流时,要给另一条退流。
-
如果正向边的流量增加,那么正向边可以反悔的流量就变多了,反向边的剩余容量就要增加对应的流量。因为反向边的容量为 \(0\),流量为负,为了增加反向边的剩余容量,就应该减少反向边的流量。
-
如果反向边的流量增加,那么反向边的剩余容量减少了,说明我们把正向边的流量反悔了一部分做其他用途了,因此应当减少正向边的流量。
如果还不懂,可以看这张图:假设每条边的容量为 \(1\):

讲到这里我们还不能严谨证明这种方法的正确性,但大家可以感性理解一下 (就像 2025CSP-ST1一样)。实际上这种方法的正确性等价于最大流最小割定理,下文会讲。
直接暴力地进行这种增广的复杂度最坏是 \(O(m|f|)\) 的,\(|f|\) 是最大流的大小。这样的复杂度非常不优秀,因此我们要使用各种优化来维护复杂度。
Edmonds-Karp(EK)算法
这个算法的核心在于用 BFS 替代 DFS 来找增广路。算法流程如下:
-
将源点放入队列。
-
开始在残量网络上 BFS(即不走流量等于容量的边),记录每个节点是从哪条边 BFS 到的,以及在 BFS 树上每个节点到源点的路径上最少的剩余容量。
-
如果 BFS 到了汇点,则找到了一条增广路,从汇点回溯到源点增流即可(刚刚记了每个节点是从哪条边 BFS 到的,这时有用了)。
-
如果 BFS 不到汇点,无法增广,算法结束。
这样做的复杂度为 \(O(nm^2)\),但严谨的证明简直不是人看的过于困难,有兴趣到这去看。
结合代码更好懂
//代码来源 OI-Wiki,肯定不是因为我懒得写~~~
constexpr int MAXN = 250;
constexpr int INF = 0x3f3f3f3f;struct Edge {int from, to, cap, flow;Edge(int u, int v, int c, int f) : from(u), to(v), cap(c), flow(f) {}
};struct EK {int n, m; // n:点数,m:边数vector<Edge> edges; // edges:所有边的集合vector<int> G[MAXN]; // G:点 x -> x 的所有边在 edges 中的下标int a[MAXN], p[MAXN]; // a:点 x -> BFS 过程中最近接近点 x 的边给它的最大流// p:点 x -> BFS 过程中最近接近点 x 的边void init(int n) {for (int i = 0; i < n; i++) G[i].clear();edges.clear();}void AddEdge(int from, int to, int cap) {edges.push_back(Edge(from, to, cap, 0));edges.push_back(Edge(to, from, 0, 0));m = edges.size();G[from].push_back(m - 2);G[to].push_back(m - 1);}int Maxflow(int s, int t) {int flow = 0;for (;;) {memset(a, 0, sizeof(a));queue<int> Q;Q.push(s);a[s] = INF;while (!Q.empty()) {int x = Q.front();Q.pop();for (int i = 0; i < G[x].size(); i++) { // 遍历以 x 作为起点的边Edge& e = edges[G[x][i]];if (!a[e.to] && e.cap > e.flow) {p[e.to] = G[x][i]; // G[x][i] 是最近接近点 e.to 的边a[e.to] =min(a[x], e.cap - e.flow); // 最近接近点 e.to 的边赋给它的流Q.push(e.to);}}if (a[t]) break; // 如果汇点接受到了流,就退出 BFS}if (!a[t])break; // 如果汇点没有接受到流,说明源点和汇点不在同一个连通分量上for (int u = t; u != s;u = edges[p[u]].from) { // 通过 u 追寻 BFS 过程中 s -> t 的路径edges[p[u]].flow += a[t]; // 增加路径上边的 flow 值edges[p[u] ^ 1].flow -= a[t]; // 减小反向路径的 flow 值}flow += a[t];}return flow;}
};
这个算法仍然不够强力,遇到稠密图就倒闭了。我们需要一个更好的算法。
Dinic 算法
Dinic 算法在 DFS 前先用 BFS 建层次图,并且 DFS 找的也不是增广路,而是阻塞流。慢慢来,先讲层次图是什么:
层次图
层次图就是从源节点开始在残量网络上 BFS,记录每个节点的深度(从源节点走最少多少条边能到这个节点),最后只保留起终点深度差为 \(1\) 的有向边的图。像这样:

图中所有黑边是不被层次图保留的。
阻塞流
接下来讲阻塞流,阻塞流就是增加任意一条边的流量就会爆炸的流。注意阻塞流只是不能增加,并不是最大流。你可以看作若干条增广路的并。如何用 DFS 找阻塞流呢?
-
在 DFS 状态中记录当前节点和流入这个点的流量。初始为 \(\text{dfs}(s,\inf)\);
-
对于 DFS 到的节点,枚举出边,把尽可能多的流量塞给这条出边(这条边的剩余容量和流入当前节点的流量的 min);
-
当然,这条边不一定会把这么多流量都使用,此时 DFS 返回了这条边用这些流量后产生的阻塞流大小;
-
把这条边流量增加阻塞流大小,反向边(忘了翻回去看)相应地退流,并减少流入这个点的流量
-
如果当前节点是汇点/到这个节点时已经没流了/所有出边都阻塞了/流入这个点的剩余流量用光了,返回阻塞流大小。
当前弧优化
这样求阻塞流仍然不够。如果某个点有很多入边和出边,每次把出边都遍历一遍,复杂度爆炸了。因此,对于每个节点,我们还要记录第一条还没有被阻塞的出边,即当前弧。每次 DFS 到这个节点直接从当前弧枚举出边就行了。
在代码实现上,链式前向星就体现出优势了:当我们使用 for(int i=head[x];i;i=nxt[i]) 遍历出边时,只需把 i 定义成引用(for(int &i=head[x];i;i=nxt[i]),由于每枚举一条边就会阻塞一条边,这样 i 更新的时候 head[x] 也会更新,head[x] 就是 x 的当前弧了。
所以,我们得到了 Dinic 算法的全过程:
-
在残量网络上用 BFS 建层次图;
-
在层次图上跑 DFS 求阻塞流大小,加入答案;
-
重复以上过程直到 BFS 跑不到汇点为止。
这个 B 站视频将 Dinic 的过程可视化了,非常直观,值得一看:
参考代码
//Luogu P3376 【模板】网络最大流
#include <bits/stdc++.h>
#define N 205
#define M 5005
#define inf 0x3f3f3f3f3f3f3f3f
using namespace std;
int n,now,m,S,T,head[N],to[M*2],nxt[M*2],dep[N],cur[N];
long long flow[M*2],cap[M*2];
//head to nxt 原图
//cur 当前弧
//dep 层次图深度
//flow 边的流量
//cap 边的容量
//S T 源汇点
void add(int x,int y,long long fl,long long c)//加有向边
{to[++now]=y;nxt[now]=head[x];head[x]=now;flow[now]=fl;cap[now]=c;
}
void addedge(int x,int y,long long c)//加正反向边
{add(x,y,0,c);add(y,x,0,0);return;
}
int bfs()//建层次图
{queue<int> q;q.push(S);memset(dep,0,sizeof(dep));dep[S]=1;while(q.size()){int now=q.front();q.pop();for(int i=head[now];i;i=nxt[i])//枚举出边{int v=to[i];if((!dep[v])&&(cap[i]>flow[i]))//如果这条边还有剩余流量(保证在残量网络上 bfs){dep[v]=dep[now]+1;q.push(v);}}}return dep[T];
}
long long dfs(int x,long long fl)
{if(x==T||(!fl)) return fl;//特判long long use=0;//流入这个点的流量用了多少for(int& i=cur[x];i;i=nxt[i])//枚举出边,当前弧优化{int v=to[i];long long d;//出点的阻塞流大小if((dep[v]==dep[x]+1)&&(d=dfs(v,min(cap[i]-flow[i],fl-use))))//只能走层次图上的边{//更新信息use+=d;flow[i]+=d;flow[((i-1)^1)+1]-=d;if(use==fl) return use; //流入这个点的流量用完了}}return use;//返回阻塞流大小
}
long long dinic()
{long long maxflow=0;while(bfs())//bfs 直到 t 的 dep 为 0{memcpy(cur,head,sizeof(head));//初始化当前弧maxflow+=dfs(S,inf);//加上阻塞流}return maxflow;
}
int main()
{scanf("%d%d%d%d",&n,&m,&S,&T);for(int i=1;i<=m;i++){int u,v;long long c;scanf("%d%d%lld",&u,&v,&c);addedge(u,v,c);}printf("%lld\n",dinic());return 0;
}
Dinic 算法的复杂度最差是 \(O(n^2m)\) 的,但只有在特殊性质的图上会卡满,在一般的图上,它的平均效率非常优秀。由于网络流一般用于建模,不会建出卡满的图的,因此可以说是最优秀的最大流算法。复杂度证明见此。
3. 最小割问题
最大流最小割定理
最大流最小割定理的内容非常简单:在网络中,最大流的大小等于最小割的大小。证明也不是很难,如下:
所以 \(|f|\le||S,T||\)
第一个小于等于号取等的条件是所有起点子在 \(T\),终点在 \(S\) 的边均没有流,第二个小于等于号取等的条件是所有起点在 \(S\),终点在 \(T\) 的边都满流。
令原图为 \(G\),残量网络为 \(G_f\),边 \((u,v)\) 的剩余容量为 \(c_f(u,v)\)。设增广结束后在 \(G_f\) 上 \(s\) 和 \(t\) 不连通,此时将 \(s\) 连通的点划到 \(S\) 集合,其余划到 \(T\) 集合,容易发现对于任意 \(u\in S\),\(v\in T\),有 \(c_f(u,v)=0\),则考虑所有这些边在原图中是正向边还是反向边:
-
\((u,v)\) 是正向边:则 \(c_f(u,v)=c(u,v)-f(u,v)=0\) 即 \(c(u,v)=f(u,v)\),这使得第二个小等号等号成立;
-
\((u,v)\) 是反向边:则 \(c_f(u,v)=c(u,v)-f(u,v)=0-f(u,v)=f(v,u)=0\),这使得第一个小等号等号成立。
所以增广结束时得到的流 \(f\) 满足 \(|f|=||S,T||\),由于一张图中的最小割大小显然为定值,所以此时 \(|f|\) 取到最大值,是最大流。定理得证,同时证明了 Ford–Fulkerson 增广的正确性。
知道这个定理后,我们就能搞些“曲线救国”的骚操作了。具体地,有的题建了网络流要求最大流,会发现求最大流的复杂度不可接受,但转化为最小割问题后,就能利用图的特殊性质高效率解决。
例题
P4001 狼抓兔子
题目大意
给定如图所示的无向图,求左上角到右下角的最小割。

题目解法
发现这是一张平面图(除了顶点无边相交的图),平面图最小割可以转化成对偶图最短路问题,如图:

【图源:https://www.luogu.com.cn/article/37vytrcv】
对偶图上的边的边权等于与其相交的原图边的权值,那么把起点到终点的一条路径上交的原图边全部断掉就是一个割的方案,最小割就是最短路了。
点击查看代码
//输出 long long 的时候用 %lld 了吗 ~~~
//交之前改 freopen 了吗 ~~~
//改完代码及时交了吗 ~~~
//算了内存不会 MLE 了吗 ~~~
//T1 卡住看 T2 了吗 ~~~
#include <bits/stdc++.h>
#define N 2000005
#define M 8000005
using namespace std;
int n,m,now,head[N],to[M],nxt[M];
long long w[M],l,dis[N];
void add(int x,int y,long long z)
{to[++now]=y;nxt[now]=head[x];head[x]=now;w[now]=z;return;
}
void addedge(int x,int y,long long z)
{add(x,y,z);add(y,x,z);
}
int id(int x,int y,int op)
{if(x<=0||y>=m) return 0;if(x>=n||y<=0) return (n-1)*(m-1)*2+1;return ((x-1)*(m-1)+y)*2-1+op;
}
struct node
{int p;long long dis;bool operator <(const node& b)const{return dis>b.dis;}
};
priority_queue<node> q;
int main()
{scanf("%d%d",&n,&m);for(int i=1;i<=n;i++){for(int j=1;j<=m-1;j++){scanf("%lld",&l);addedge(id(i,j,1),id(i-1,j,0),l);}}for(int i=1;i<=n-1;i++){for(int j=1;j<=m;j++){scanf("%lld",&l);addedge(id(i,j,0),id(i,j-1,1),l);}}for(int i=1;i<=n-1;i++){for(int j=1;j<=m-1;j++){scanf("%lld",&l);addedge(id(i,j,0),id(i,j,1),l);}}memset(dis,0x3f,sizeof(dis));dis[0]=0;q.push({0,0});while(q.size()){int x=q.top().p;long long len=q.top().dis;q.pop();if(dis[x]<len) continue;for(int i=head[x];i;i=nxt[i]){int v=to[i];if(dis[v]>dis[x]+w[i]){dis[v]=dis[x]+w[i];q.push({v,dis[v]});}}}printf("%lld\n",dis[(n-1)*(m-1)*2+1]);return 0;
}
CF724E Goods transportation
题目大意
\(n\) 个城市,第 \(i\) 个城市生产了 \(p_i\) 的货物,最多出口 \(s_i\) 的货物,若 \(i<j\),则城市 \(i\) 最多可以给城市 \(j\) 运 \(c\) 的货物,求这 \(n\) 个城市的最大出口量。
题目解法
首先容易有网络流建图:源点向城市 \(i\) 有 \(p_i\) 容量的边,城市 \(i\) 向汇点有 \(s_i\) 容量的边,城市两两之间编号小的向编号大的连容量为 \(c\) 的边,则答案就是最大流。
跑最大流还是太不优了,考虑转化为最小割,令 DP 状态 f_{i,j} 表示前 \(i\) 个城市有 \(j\) 个属于 \(S\) 集合,若 \(i\) 属于 \(S\) 集合,则它会产生 \(s_i\) 的容量,若 \(i\) 属于 \(T\) 集合,则它会产生 \(p_i+c\times j\) 的容量,于是
直接 DP \(O(n^2)\) 就能过了。
参考文章
https://www.luogu.com.cn/article/loaga2f1
https://www.luogu.com.cn/article/h6p6ar33
https://oi-wiki.org/graph/planar/
https://oi-wiki.org/graph/flow/
https://oi-wiki.org/graph/flow/max-flow/
https://oi-wiki.org/graph/flow/min-cut/