网络流
网络流
模型框架
现实模型:自然水供水系统。它相当于水管调度员。
- 问题的本质是解决有向图中“资源分配与路径优化”的问题。
定义
两个特殊节点:源点(\(S\))、汇点(\(T\))。
边的关键属性:容量 \(c\)。
- 每条有向边都有一个非负实数“容量”。
性质
- “流”作为资源的实际分配,必须满足两个核心规则,否则不是合法的网络流。
- 容量约束:任意一条边上的实际流量 (\(f\)),不能超过该边的容量(\(c\)),即 \(f \le c\)。例如,一根容量为 10 t 的水管,实际输水不能超过 10 t。
- 流量守恒:除了源点和汇点,其他所有中转节点的“流入流量总和”必须等于“输出流量总和”。
- 反对称性约束(斜对称):对任意 \(u,v \in V\),\(f(u,v) = -f(v,u)\)。
- 弧流量约束(容量限制):\(0 \le f(u,v) \le c(u,v)\)。
最大流:流量最大的那个流。
最小割:就是求得一个割 \((S,T)\) 使得割的容量 \(c(S,T)\) 最小。
最大流求法
- 最大流德核心遵循 Ford-Fulkerson 框架:不断在“残留网络”中找“增广路”(能从源点流向汇点且有剩余容量的路径)。
- EK 算法适用于节点数 \(\le 1000\)、边数 \(\le 10^4\) 的小规模网络,时间复杂度 \(O(F \times E)\)(\(F\) 是最大流,\(E\) 是边数),流量大时效率低。
Dinic 算法:
- 核心思路
在 Ford-Fulkerson 框架上做两个优化:
- 分层(BFS):先给节点按“到源点的最短距离”分层,保证增广路是“最短路径”;
- 多路增广(DFS):在分层图中用 DFS 批量找增广路,且用“当前弧优化”避免重复遍历有效边。
- 大体流程为:
- 用 bfs 求出 G 中每个点的深度。
- 用 dfs 在 bfs 所跑出来的途中,枚举一条从源点到汇点的路径 P,求出 \(limit = \min{(c \in P)}\),将路径上的每条边都减去 \(limit\),并新建一条反边,权值为 \(limit\)。
- 当前弧优化
这里的“弧”指的就是一个点 \(x\) 向其它点连的有向出边。
当我们选择一个点 \(x\) 的第 \(i\) 条弧作为一条增广路径的一部分时,前 \(i-1\) 条边一定已经能流的都流了,所以考虑标记用到哪条弧,下次 dfs 就可以跳过这些弧,提升程序效率。
复杂度分析
注:不知道严不严谨。(来自 Alex_Wei 的好文)
引理 1
每次增广后的 \(S\) 到各个点的最短路不减。
证明
考虑每次加入新边的方向,因为是反向边,所以是从 \(dis\) 大的点指向 \(dis\) 小的点。
如果最短路长度减少,那么在新的最短路中(记为 \(dis'\))一定存在 \((x,y)\) 满足 \(dis_x +1 <dis_y\)。那么说明 \((x,y)\) 不在原残量网络中,否则不满足三角形不等式(\(dis_y \ge dis_x + w\),\(w\) 为 \((x,y)\) 的边权)。
由于 \((x,y)\) 是新加入的边,说明 \((y,x)\) 在上一轮被增广,而增广需要满足 \(dis_x =dis_y +1\)。
于是将 \(dis_x = dis_y +1\) 代入 \(dis_x + 1 <dis_y\),得到 \(dis_y +2 < dis_y\),矛盾,故引理 1 成立。
引理 2
Dinic 的每次增广都会使 \(S\) 到 \(T\) 的最短路增加。
证明
反证法。设 \(S\) 到 \(T\) 的最短路不增加。由引理 1 知 \(S\) 到 \(T\) 的最短路不变。
那么这一条最短路 \(P\) 一定不是原残量网络中的最短路(否则会被增广掉),于是必然存在一个 \((x,y)\),满足 \(dis_x + 1 < dis_y\)(三角不等式在原残量网络上恒成立的逆命题),由类似引理 1 的方法可以导出矛盾,故引理 2 成立。
于是可以得到增广次数是 \(O(n)\) 的,因为每次增广都会让增广路上剩余流量最小的边满流(相当于这条边在新图上被删了。这里并不是真正的删掉了,只是因为容量为 \(0\) 被跳过不访问,相当于删掉)。我们把这种边称为关键边,一条关键边可以进行一轮增广,所以增广轮数是 \(O(m)\) 的,增广路的长度是 \(O(n)\)。
故 Dinic 的时间复杂度为 \(O(n^2m)\),但是这个上界非常松,这也是为什么 Dinic 算法在求网络最大流时表现良好。
模板代码
#include<bits/stdc++.h>
#define int long long
#define rep(i,l,r) for(int i=(l);i<=(r);++i)
using namespace std;
const int INF=1e9;
const int M=1e5+10,N=1e4+10;
int n,m,S,T,tot=1;
int head[N],del[N],dis[N];
struct edge{
int to,nxt,w;
}e[M<<2];
void add(int u,int v,int w){
++tot;
e[tot].w=w;
e[tot].to=v;
e[tot].nxt=head[u];
head[u]=tot;
}
bool bfs(){
rep(i,1,n){
dis[i]=INF;
}
queue<int> q;
dis[S]=0;
del[S]=head[S];
q.push(S);
while(!q.empty()){
int x=q.front();
q.pop();
for(int i=head[x];i!=0;i=e[i].nxt){
int y=e[i].to,w=e[i].w;
if(dis[y]!=INF || w<=0)continue;
dis[y]=dis[x]+1;
del[y]=head[y];
q.push(y);
if(y==T)return 1;
}
}
return 0;
}
int dfs(int x,int limit){
if(x==T)return limit;
int flow=0,p;
for(int i=del[x];i!=0;i=e[i].nxt){
if(limit<=0)break;
del[x]=i;
int y=e[i].to,w=e[i].w;
if((dis[y]!=dis[x]+1) || w<=0)continue;
p=dfs(y,min(limit,w));
if(p==0){
dis[y]=INF;
continue;
}
e[i].w-=p;
e[i^1].w+=p;
flow+=p;
limit-=p;
}
return flow;
}
int dinic(){
int res=0;
while(bfs()){
res+=dfs(S,INF);
}
return res;
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);cout.tie(nullptr);
cin>>n>>m>>S>>T;
while(m--){
int u,v,c;
cin>>u>>v>>c;
add(u,v,c);
add(v,u,0);
}
cout<<dinic()<<"\n";
return 0;
}
对网络最大流的理解
实际上是反悔贪心,我们如果在增广时选择了不是最优的路径去增广,我们可以通过反向边进行反悔。下面是来自 OI-wiki 的一张图:

最小割
多源汇最大流
顾名思义,就是给出若干个源点 \(s_i\) 和若干个汇点 \(t_i\),求网络的最大流。
- 考虑图论建模,建出一张新图 \(G'\),其中在 \(G'\) 的源点 \(S\) 向原图 \(G\) 中的每个源点连容量为 INF 的边,每个汇点 \(t_i\) 向 \(G'\) 的汇点 \(T\) 连容量为 INF 的边。此时新图的最大流等于原图的最大流。
Proof
证明思想:考虑证明:
- 对于 \(G'\) 中的每一条可行流,都对应着 \(G'\) 中的一条可行流。
- 对于 \(G\) 中的每一条可行流,都对应着 \(G'\) 中的一条可行流。
- 于是得到 \(G\) 和 \(G'\) 中的可行流一一对应,自然就有原图的最大流等于新图的最大流。
-
对于 \(G'\) 中的一条可行流,因为除了 \(S,T\) 之外的点都满足流量守恒和容量限制,所以一定对应原图中的一条可行流。
-
对于 \(G\) 中的每一条可行流进行考虑。对于 \(G\) 中的点 \(v\) 满足 \(v \in \{ V- \{s_i,t_i \} \}\),都满足容量限制和流量守恒。在 \(G'\) 中,\(S\) 和 \(T\) 补偿了原图的源点汇点没有满足流量守恒的情况。(从 \(S\) 流出的容量为 INF 的边补偿了 \(s_i\) 的入边流量,\(T\) 同理),所以 \(G'\) 中的每一条可行流也对应着 \(G\) 中的一条可行流。
所以 \(G'\) 的可行流与 \(G\) 的可行流一一对应,由此可得,\(G\) 的最大流等于 \(G'\) 的最大流。
Code
#include<bits/stdc++.h>
#define int long long
#define rep(i,l,r) for(int i=(l);i<=(r);++i)
using namespace std;
const int INF=1e9;
const int M=2e5+10,N=5e4+10;
int n,m,S,T,tot=1,Sc,Tc;
int head[N],del[N],dis[N];
struct edge{
int to,nxt,w;
}e[M<<2];
void add(int u,int v,int w){
++tot;
e[tot].w=w;
e[tot].to=v;
e[tot].nxt=head[u];
head[u]=tot;
}
bool bfs(){
rep(i,0,n+1){
dis[i]=INF;
}
queue<int> q;
dis[S]=0;
del[S]=head[S];
q.push(S);
while(!q.empty()){
int x=q.front();
q.pop();
for(int i=head[x];i!=0;i=e[i].nxt){
int y=e[i].to,w=e[i].w;
if(dis[y]!=INF || w<=0)continue;
dis[y]=dis[x]+1;
del[y]=head[y];
q.push(y);
if(y==T)return 1;
}
}
return 0;
}
int dfs(int x,int limit){
if(x==T)return limit;
int flow=0,p;
for(int i=del[x];i!=0;i=e[i].nxt){
if(limit<=0)break;
del[x]=i;
int y=e[i].to,w=e[i].w;
if((dis[y]!=dis[x]+1) || w<=0)continue;
p=dfs(y,min(limit,w));
if(p==0){
dis[y]=INF;
continue;
}
e[i].w-=p;
e[i^1].w+=p;
flow+=p;
limit-=p;
}
return flow;
}
int dinic(){
int res=0;
while(bfs()){
res+=dfs(S,INF);
}
return res;
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);cout.tie(nullptr);
cin>>n>>m>>Sc>>Tc;
S=0,T=n+1;
while(Sc--){
int x;
cin>>x;
add(S,x,INF);
add(x,S,0);
}
while(Tc--){
int x;
cin>>x;
add(x,T,INF);
add(T,x,0);
}
while(m--){
int u,v,c;
cin>>u>>v>>c;
add(u,v,c);
add(v,u,0);
}
cout<<dinic()<<"\n";
return 0;
}
参考资料
- OI-Wiki。
- llr 老师的 PPT。
- Acwing 的题解与进阶课。
- Alex_Wei 好文。

浙公网安备 33010602011771号