【笔记】网络流Ⅰ:最大流最小割
前言:本文内容原创,转载请注明出处。
引
网络流理论(network-flows)是一种类比水流的解决问题方法,与线性规划密切相关。网络流的理论和应用在不断发展,出现了具有增益的流、多终端流、多商品流以及网络流的分解与合成等新课题。网络流的应用已遍及通讯、运输、电力、工程规划、任务分派、设备更新以及计算机辅助设计等众多领域。
以上内容来自百度百科。
最大流可以解决什么问题?
一个简单例子:有个自来水场,水厂到你家可能要经过好多中转站和水管,每条水管有一个流量限度,就是水管最多能流多少单位的水。自来水厂源源不断的放水,问你家最多能收到几个单位的水。
一些定义
-
容量网络:一个有向网络(有向图)
,指定一个顶点,称为源点(记为 ),以及另一个顶点,称为汇点(记为 );对于每一条弧 属于 ,对应有一个边权 ,称为 弧的容量 。这样的有向网络 称为容量网络。 -
弧的流量:通过容量网络
中每条弧 的实际流量(简称流量),记为 。 -
网络流:所有弧上流量的集合
,称为该容量网络的一个网络流。 -
可行流:在容量网络
中满足以下条件的网络流 ,称为可行流。-
弧流量限制条件:
-
平衡条件:即流入一个点的流量要等于流出这个点的流量,(源点和汇点除外)
-
-
最大流:在容量网络中,满足弧流量限制条件,满足平衡条件,并且具有最大流量的可行流,称为网络最大流,简称最大流。
-
割:设网络中一些边的集合为
,断开这些边,若能将网络分成分别包含源点和汇点的两个子集,则 为网络的割。 -
最小割:边权值之和最小的割。
以上图为例,源点为
可以发现,图中所示的是可行的一种最大流。
最大流算法
首先,显而易见的,单独一条路径上的流量(链上)由其边权的最小值决定。
所以最简单的想法是,从源点开始
但该方法存在明显缺陷:
如下面的典例:
其答案显然为
这种做法出现错误,是因为其陷入了局部最优解。所以,我们需要反悔的机会。
Ford-Fulkerson 算法
考虑引入反向边。这是所有最大流算法的核心。
何为反向边?
字面意思,对于每一条现有的弧,建一条与它起点终点互换的弧,其权值初始为
仍然以上图为例:
反向边有何作用?
每次求出一个流量的增加值
其中红色为已用流量,黑色为正向边剩余容量,绿色为反向边剩余容量。
这样
第二次走的流量用紫色标出。其中黄圈内的是这次路径反向增加的容量。
可以发现,这时剩余容量已不支持找到一条从源点到汇点的可行路径了,所以得出答案为
如何理解其正确性呢?
对于上图,我们删掉一些无用内容,包括正反重复走的边(正负抵消)、没有走到过的边等。
可以发现,就是我们手算得到的结果。
例1:B3606 [图论与代数结构 501] 网络流_1
考虑到点数边数极小,边权随机,可以直接硬上
其中,
AC Code
#include<bits/stdc++.h>
#define IOS ios::sync_with_stdio(false)
#define TIE cin.tie(0),cout.tie(0)
#define int long long
#define inf 1e18
using namespace std;
int n,m,s,t,res[35][35];
int u,v,c;
bool vis[35];
int dfs(int x,int t,int f){
if(x==t) return f; //到终点了返回
for(int i=1;i<=n;i++){
if(res[x][i]>0&&!vis[i]){ //有剩余容量且当前次未访问
vis[i]=1;
int tmp=dfs(i,t,min(f,res[x][i])); //流量取min
if(tmp>0){
res[x][i]-=tmp,res[i][x]+=tmp; //正向减,反向加
return tmp;
}
}
}
return 0;
}
int maxflow(int s,int t){
int ans=0;
while(1){
memset(vis,0,sizeof(vis)); //每次找路径每个点只要访问一次,所以计 vis
vis[s]=1;
int tmp=dfs(s,t,inf);
if(tmp==0) return ans; //无路可走
ans+=tmp;
}
}
signed main(){
IOS;TIE;
cin>>n>>m>>s>>t;
for(int i=1;i<=m;i++) cin>>u>>v>>c,res[u][v]+=c; //注意肯有重复边,要 +=
cout<<maxflow(s,t)<<endl;
return 0;
}
为什么上面提到了 边权随机 呢?
因为该算法存在一定问题:其复杂度问号,容易被卡。如下例:
它要跑
如何优化?
EK 算法
是求最大流的一种容易实现、代码易懂的算法。
考虑引入
每次从源点尝试找到一条到达汇点的路径,若路径上最小的残留量大于
设点数为
代码可以参考 这篇博客,这里不再给出。
Dinic 算法
考虑进行 多路增广。
何为 多路增广?
就是每次根据剩余容量构建一张分层图,在层次图中使用
实现:每次用
为什么它的复杂度会更优?
再以此图为例。
注意,
复杂度上限为
例2:B3607 [图论与代数结构 502] 网络流_2
因为点数同样很少,所以偷懒还是邻接矩阵存图。
AC Code
#include<bits/stdc++.h>
#define IOS ios::sync_with_stdio(false)
#define TIE cin.tie(0),cout.tie(0)
#define int long long
#define inf 1e18
using namespace std;
int n,m,s,t,res[105][105],dep[105];
int u,v,c;
queue<int> q;
bool bfs(int s,int t){
while(q.size()) q.pop();
q.push(s);
memset(dep,0,sizeof(dep));
dep[s]=1;
while(q.size()){
int tmp=q.front();q.pop();
for(int i=1;i<=n;i++){
if(res[tmp][i]&&!dep[i]){ //有容量剩余
dep[i]=dep[tmp]+1; //求深度
q.push(i);
}
}
}
return dep[t];
}
int dfs(int x,int t,int f){
if(x==t) return f;
int ans=0;
for(int i=1;i<=n;i++){
if(res[x][i]&&dep[x]+1==dep[i]){ //有容量剩余且深度连续
int tmp=dfs(i,t,min(res[x][i],f));
res[x][i]-=tmp,res[i][x]+=tmp; //正向减,反向加
ans+=tmp,f-=tmp;
if(f<=0) break; //小优化:无剩余就可以跳过了
}
}
return ans;
}
int dinic(int s,int t){
int ans=0;
while(bfs(s,t)){ //还有增广路径
int tmp=dfs(s,t,inf);
ans+=tmp;
}
return ans;
}
signed main(){
IOS;TIE;
cin>>n>>m>>s>>t;
for(int i=1;i<=m;i++) cin>>u>>v>>c,res[u][v]+=c;
cout<<dinic(s,t)<<endl;
return 0;
}
双倍经验:P3376【模板】网络最大流
三倍经验:P1343 地震逃生
链星&当前弧优化
因为点数肯能会很多,所以用邻接表存图不现实。
笔者一开始想到使用
于是笔者写了一个复杂度问号的东西:
int dfs(int x,int t,int f){
if(x==t||!f) return f;
int ans=0;
for(int i=0;i<a[x].size();i++){
node &tmp=a[x][i];
if(tmp.f&&dep[x]+1==dep[tmp.to]){
int tt=dfs(tmp.to,t,min(tmp.f,f));
tmp.f-=tt;
for(int j=0;j<a[tmp.to].size();j++){ //暴力找反向边(
if(a[tmp.to][j].to==x){
a[tmp.to][j].f+=tt;
break;
}
}
ans+=tt,f-=tt;
if(f<=0) break;
}
}
return ans;
}
最离谱的是,P2763 试题库问题 居然这样艹过去了。
顺带一提,这题数据好像水的离谱,一开始笔者反向边加写成减居然也过了。
回归正题,不得不用链式前向星,否则反向边加权最劣也变成了
考虑两点间的正反边连续来建,这样边
再来说当前弧优化。
当前弧优化就是说我们在每次
例3:CF1423B Valuable Paper
首先考虑二分答案,二分出一个时间
为什么可以用最大流解决?
考虑建图:首先定义一个超级源点,超级原点向所有工厂连边权为
这时,若超级源点到超级汇点的最大流值为
AC Code
#include<bits/stdc++.h>
#define IOS ios::sync_with_stdio(false)
#define TIE cin.tie(0),cout.tie(0)
#define int long long
#define inf 1e18
using namespace std;
int k,n,m,p,x,to,dep[20005];
int head[20005],tot,cur[20005];
struct node{
int to,nxt,f;
}a[1000005];
void add(int u,int v,int f){
a[++tot]={v,head[u],f};
head[u]=tot;
}
struct edge{
int u,v,w;
}e[100005];
queue<int> q;
int bfs(int s,int t){
while(q.size()) q.pop();
q.push(s);
memset(dep,0,sizeof(dep));
dep[s]=1;
while(q.size()){
int tmp=q.front();q.pop();
for(int i=head[tmp];i;i=a[i].nxt){
node tt=a[i];
if(tt.f&&!dep[tt.to]){
dep[tt.to]=dep[tmp]+1;
q.push(tt.to);
}
}
}
return dep[t];
}
int dfs(int x,int t,int f){
if(x==t||!f) return f;
int ans=0;
for(int i=cur[x];i;i=a[i].nxt){
node &tmp=a[i];
cur[x]=i; //更新 cur 数组
if(tmp.f&&dep[x]+1==dep[tmp.to]){
int tt=dfs(tmp.to,t,min(tmp.f,f));
tmp.f-=tt,a[i^1].f+=tt; //反向边编号为 i^1
ans+=tt,f-=tt;
if(f<=0) break;
}
}
return ans;
}
int dinic(int s,int t){
int ans=0;
while(bfs(s,t)){
memcpy(cur,head,sizeof(head)); //cur 数组还原
ans+=dfs(s,t,inf);
}
return ans;
}
void build(int x){ //建图
tot=1;
for(int i=0;i<=to;i++) head[i]=0;
for(int i=1;i<=n;i++){ //超级原点向所有工厂连边权为 1 的边
add(0,i,1),add(i,0,0);
}
for(int i=n+1;i<=n+n;i++){ //所有机场向超级汇点连边权为 1 的边
add(i,to,1),add(to,i,0);
}
for(int i=1;i<=m;i++){
if(e[i].w<=x){ //联通的工厂、机场间连边
add(e[i].u,e[i].v+n,1);
add(e[i].v+n,e[i].u,0);
}
}
}
bool check(int x){
build(x);
return dinic(0,to)==n; //最大流 =n 则可行
}
signed main(){
IOS;TIE;
cin>>n>>m;
for(int i=1;i<=m;i++) cin>>e[i].u>>e[i].v>>e[i].w;
to=n*2+1;
int l=0,r=1e9;
while(l<=r){ //二分答案
int mid=(l+r)>>1;
if(check(mid)) r=mid-1;
else l=mid+1;
}
if(l==1e9+1) cout<<-1<<endl; //无解
else cout<<l<<endl;
return 0;
}
最小割
引理:
在一个网络流中,能够从源点到达汇点的最大流量等于如果从网络中移除就能够导致网络流中断的边的集合的最小容量和。即在任何网络中,最大流的值等于最小割的容量。
一句话:最大流
为什么最大流是最小割?
个人感性理解:
最大流意味着无法增广,也就是说所以路径都被某几条边的容量限制住了。这些边的流量已经达到了容量上限,而最大流就是这些边的流量之和(瓶颈)。同时,这些边一定构成了原网络的一个割。它是最小的割,因为其他边不一定达到了容量上限,若舍弃这些边去选其他边,边权和只增。
例4:ABC010D 浮気予防
原题可以转化:
设高桥为超级源点。考虑将所有女生向超级汇点连权值为
因为数据范围小,所以仍然偷懒用链接矩阵。
AC Code
#include<bits/stdc++.h>
#define IOS ios::sync_with_stdio(false)
#define TIE cin.tie(0),cout.tie(0)
#define int long long
#define inf 1e18
using namespace std;
int n,g,e,s,t,res[205][205],dep[205];
int u,v,c;
queue<int> q;
bool bfs(int s,int t){
while(q.size()) q.pop();
q.push(s);
memset(dep,0,sizeof(dep));
dep[s]=1;
while(q.size()){
int tmp=q.front();q.pop();
for(int i=1;i<=n;i++){
if(res[tmp][i]&&!dep[i]){
dep[i]=dep[tmp]+1;
q.push(i);
}
}
}
return dep[t];
}
int dfs(int x,int t,int f){
if(x==t) return f;
int ans=0;
for(int i=1;i<=n;i++){
if(res[x][i]&&dep[x]+1==dep[i]){
int tmp=dfs(i,t,min(res[x][i],f));
res[x][i]-=tmp,res[i][x]+=tmp;
ans+=tmp,f-=tmp;
if(f<=0) break;
}
}
return ans;
}
int dinic(int s,int t){
int ans=0;
while(bfs(s,t)){
int tmp=dfs(s,t,inf);
ans+=tmp;
}
return ans;
}
signed main(){
IOS;TIE;
cin>>n>>g>>e;
for(int i=1;i<=g;i++) cin>>u,res[u][n]++;
for(int i=1;i<=e;i++){
cin>>u>>v;
res[u][v]++,res[v][u]++;
}
if(g==0) cout<<0<<endl;
else cout<<dinic(0,n)<<endl;
return 0;
}
例5:P3931 SAC E#1 - 一道难题 Tree
设树根为源点,考虑将所有叶子结点向超级汇点连边,权值为无限大。
原题可以转化:删去一些边,使树根和超级汇点分处于两个子集的最小代价,也就是最小割。
找出叶子结点的方法:度为
连边方法:因为给出的顺序不一定是
AC Code
#include<bits/stdc++.h>
#define IOS ios::sync_with_stdio(false)
#define TIE cin.tie(0),cout.tie(0)
#define int long long
#define inf 1e18
using namespace std;
int k,n,root,to,dep[100005],du[100005];
int head[100005],tot,cur[100005],u,v,w;
vector<pair<int,int> > b[100005];
struct node{
int to,nxt,f;
}a[1000005];
void add(int u,int v,int f){
a[++tot]={v,head[u],f};
head[u]=tot;
}
queue<int> q;
int bfs(int s,int t){
while(q.size()) q.pop();
q.push(s);
memset(dep,0,sizeof(dep));
dep[s]=1;
while(q.size()){
int tmp=q.front();q.pop();
for(int i=head[tmp];i;i=a[i].nxt){
node tt=a[i];
if(tt.f&&!dep[tt.to]){
dep[tt.to]=dep[tmp]+1;
q.push(tt.to);
}
}
}
return dep[t];
}
int dfs(int x,int t,int f){
if(x==t||!f) return f;
int ans=0;
for(int i=cur[x];i;i=a[i].nxt){
node &tmp=a[i];
cur[x]=i;
if(tmp.f&&dep[x]+1==dep[tmp.to]){
int tt=dfs(tmp.to,t,min(tmp.f,f));
tmp.f-=tt,a[i^1].f+=tt;
ans+=tt,f-=tt;
if(f<=0) break;
}
}
return ans;
}
int dinic(int s,int t){
int ans=0;
while(bfs(s,t)){
memcpy(cur,head,sizeof(head));
ans+=dfs(s,t,inf);
}
return ans;
}
void dfs0(int x,int fa){
for(int i=0;i<b[x].size();i++){
pair<int,int> tmp=b[x][i];
if(tmp.first==fa) continue;
add(x,tmp.first,tmp.second);
add(tmp.first,x,0);
dfs0(tmp.first,x);
}
}
signed main(){
IOS;TIE;
cin>>n>>root;
to=n+1;
for(int i=1;i<n;i++){
cin>>u>>v>>w;
du[u]++,du[v]++;
b[u].push_back(make_pair(v,w));
b[v].push_back(make_pair(u,w));
}
dfs0(root,0);
for(int i=1;i<=n;i++){
if(du[i]==1&&i!=root){
add(i,to,inf),add(to,i,0);
}
}
cout<<dinic(root,to)<<endl;
return 0;
}
其他例题
咕咕咕
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!