网络流学习笔记
开个坑,是个大工程,一篇可能放不下,所以后续存在形式未知。
每周日写一个小时,大概会写很久,目前处于一个咕咕的状态。
笔者是主要从 Alex_wei 的博客中学习网络流,因此本文有很多东西来自 wls 的博客,wls tql。
1. 一些有关概念
网络是一张有向图
流函数
- 流量限制:
,若 则称作边 满流。 - 斜对称:
。 - 流量守恒:源点汇点以外的节点不截留流量,流入流出流量相等,即
。
流
残量网络:
增广路:设
割:将
2. 最大流(Maximum flow)
2.1 前置
求解过程中一般采用贪心思想,不断寻找增广路,同时每条边能流满就流满,每条增广路上的边增加
那么看一下下面的图。
很明显,它满足条件,但这不是最优的方案。
为了避免这种错误的影响,我们在给一条边增加一定流量时,给其反边增加相等的容量,构成可反悔贪心,也被称作退流,可以结合下图理解一下这一点。
上述操作被称为一次增广。
因为操作中涉及了反边,为了方便,一般将正边与反边连续存取,通过异或操作得到反边。注意,为了保证正确性,边数初值应为
2.2 FF(ford-fulkerson)算法
复杂度
2.3 EK(Edmonds-Karp)算法
2.3.1 简述
复杂度
2.3.2 复杂度证明
引理:每次增广后的残量网络上
考虑反证,假设
设
分类讨论,若
每轮 BFS 的复杂度显然为
若
至此,EK 算法复杂度为
2.3.3 代码实现:
点击查看代码
#include<bits/stdc++.h> #define ld long double #define ll long long using namespace std; const int N=2e2+5; const int M=5e3+5; ll limit[M<<1],fl[N]; int n,m,s,t,head[N],from[M<<1],to[M<<1],tol,fr[N]; void add(int x,int y,int z) { from[++tol]=head[x]; head[x]=tol; to[tol]=y; limit[tol]=z; from[++tol]=head[y]; head[y]=tol; to[tol]=x; limit[tol]=0; } ll maxflow() { ll flow=0; while(1) { queue<int> q; memset(fl,-1,sizeof(fl)); fl[s]=1e18,q.push(s); while(!q.empty()) { int now=q.front(); q.pop(); for(int i=head[now],nxt=to[i];i;i=from[i],nxt=to[i]) if(limit[i]&&fl[nxt]==-1) fl[nxt]=min(limit[i],fl[now]),fr[nxt]=i,q.push(nxt); }//BFS增广,同时得到最小流量 fl[t] if(fl[t]==-1) return flow;//代表不存在增广路 flow+=fl[t];//计算流量和 for(int i=t;i!=s;i=to[fr[i]^1]) limit[fr[i]]-=fl[t],limit[fr[i]^1]+=fl[t];//逆推每条边流量并退流 } } int main() { ios_base::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); cin>>n>>m>>s>>t; tol=1; for(int i=1,x,y,z;i<=m;++i) cin>>x>>y>>z,add(x,y,z); cout<<maxflow()<<"\n"; return 0; }
2.4 dinic
2.4.1 简述
EK 算法每次只增广一条路径,这无疑会大大增加增广次数。dinic 算法对这一点进行优化,通过一次 BFS 对图进行分层,经过某层
当前弧优化:每个节点可能有大量出边和入边,重复的搜索会造成极大冗余,分析算法,根据贪心和 DFS 过程,如果一条边被遍历到,在它流满或与
2.4.2 复杂度证明
2.4.2.1 一般情况下复杂度上界分析
(我也不知道伪没伪,欢迎提出 hack)
即得易见平凡,仿照上例显然。留作习题答案略,读者自证不难。
首先,由于 dinic 算法包含 EK 算法,可以得知每次增广后最短路不减这条性质依然成立。
先证明单轮增广复杂度为
对于每条增广路,通过跳边被遍历时,显然跳边次数不超过
在一次增广过程中满流的边和被遍历到但没有满流或没有合法路径的边是两个不交的集合,同时它们的并一定是
因此,单轮增广跳边次数不会超过
显然,层次图的层数不可能超过
设
对于在一次增广后,在
- 若
在 中出现,则说明 尚未流满,仍有 。 - 若
在 中未出现,则说明其为 满流后退流产生的反向边,有 。
可以得出结论:这个关系在整个求解过程中始终成立。
考虑反证。假设在某次增广后
可以确定其中至少有一条边
分析
,即这条边在新一轮分层时加入 ,那么必然有增广对这两个点造成影响,又由于 需与 连通,增广的必然是 前边。
若增广 前边, , , 不变,有导出矛盾。 是退流产生的,那么在原图中 ,应用 EK 结论,得到 ,有导出矛盾。
假设不成立,因此增广轮数为
dinic 复杂度上界为
要注意的是,这个上界是非常宽松的,大多数情况下运行的复杂度很难达到这个上界,原因是题目主要考查建模,很难构造出能将 dinic 卡到上界附近的图。
2.4.2.2 特殊情况下的复杂度分析
2.4.2.2.1 单位容量网络
复杂度为
设层次编号为
假设已经进行了
2.4.2.2.2 单位容量网络,且各点入度出度均为 1
复杂度为
易于得知各条增广路之间不交。
假设已经进行了
2.4.3 代码实现:
点击查看代码
#include<bits/stdc++.h> #define ld long double #define ll long long using namespace std; const int N=2e2+5; const int M=5e3+5; int n,m,s,t,head[N],cur[N],from[M<<1],to[M<<1],dis[N],tol; ll limit[M<<1]; void add(int x,int y,int z) { from[++tol]=head[x]; head[x]=tol; to[tol]=y; limit[tol]=z; from[++tol]=head[y]; head[y]=tol; to[tol]=x; limit[tol]=0; } ll dfs(int now,ll res) { if(now==t) return res; ll flow=0; for(int i=cur[now];i&&res;i=from[i]) { cur[now]=i; int c=min(res,limit[i]),nxt=to[i]; if(dis[nxt]==dis[now]+1&&c) { int tmp=dfs(nxt,c); flow+=tmp,res-=tmp,limit[i]-=tmp,limit[i^1]+=tmp; } } if(!flow) dis[now]=-1; return flow; } ll maxflow() { ll flow=0; while(1) { queue<int> q; memcpy(cur,head,sizeof(head)); memset(dis,-1,sizeof(dis)); dis[s]=0,q.push(s); while(!q.empty()) { int now=q.front(); q.pop(); for(int i=head[now],nxt=to[i];i;i=from[i],nxt=to[i]) if(dis[nxt]==-1&&limit[i]) dis[nxt]=dis[now]+1,q.push(nxt); } if(dis[t]==-1) return flow; flow+=dfs(s,1e18); } } int main() { ios_base::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); cin>>n>>m>>s>>t; tol=1; for(int i=1,x,y,z;i<=m;++i) cin>>x>>y>>z,add(x,y,z); cout<<maxflow()<<"\n"; return 0; }
2.5 ISAP
2.5.1 简述
dinic 算法已经称得上优秀,但是每次增广后都要重新分层,显然造成了极大的时间损耗,ISAP 针对这一点进行了优化。
考虑从汇点出发进行分层,这样进行重分层更加方便。具体来说,当对节点
ISAP 中同样存在当前弧优化,并且还存在 GAP 优化,即记录层深为
2.5.2 正确性证明
和MichaelWong共同完成。
首先说明在证明中提到的边实际情况可能是一条链。
首先假设某次增广了
若
若
若
若
若
,见下:
若
综上所述,自增操作在各种情况中的正确性都可以保证,一般情况下,这样的复杂度较低。
2.5.3 代码实现
点击查看代码
#include<bits/stdc++.h> #define ld long double #define ll long long #define inf 0x3f3f3f3f using namespace std; const int N=2e2+5; const int M=5e3+5; int n,m,s,t,head[N],from[M<<1],to[M<<1],cur[M<<1],dis[N],tol,gap[N]; ll limit[M<<1]; void add(int x,int y,int z) { from[++tol]=head[x]; head[x]=tol; to[tol]=y; limit[tol]=z; from[++tol]=head[y]; head[y]=tol; to[tol]=x; limit[tol]=0; } ll dfs(int now,ll res) { if(now==t) return res; ll flow=0; for(int &i=cur[now];i&&res;i=from[i]) { int c=min(res,limit[i]),nxt=to[i]; if(dis[nxt]==dis[now]-1&&c) { int f=dfs(nxt,c); flow+=f,res-=f,limit[i]-=f,limit[i^1]+=f; } if(!res) return flow; } if(--gap[dis[now]]==0) dis[s]=n; ++dis[now],++gap[dis[now]]; return flow; } ll maxflow() { ll flow=0; queue<int> q; memset(dis,inf,sizeof(dis)); dis[t]=0,q.push(t),gap[0]=1; while(!q.empty()) { int now=q.front(); q.pop(); for(int i=head[now],nxt=to[i];i;i=from[i],nxt=to[i]) if(dis[nxt]==inf&&!limit[i]) ++gap[dis[nxt]=dis[now]+1],q.push(nxt); } while(dis[s]<n) { memcpy(cur,head,sizeof(head)); flow+=dfs(s,1e18); } return flow; } int main() { ios_base::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); cin>>n>>m>>s>>t; tol=1; for(int i=1,x,y,z;i<=m;++i) cin>>x>>y>>z,add(x,y,z); cout<<maxflow()<<"\n"; return 0; }
注意,上述三种方法的本质是大致相同的,如果初始流量为一,多次进行最大流,每次初始流量加一,可以直接考虑 EK 算法。ISAP 虽然理论上相对 dinic 较优,但在分层图中,依然需要多次分层。如[CTSC1999] 家园 / 星际转移问题,从汇点出发需要遍历的节点数是远大于从源点出发的,这时使用 dinic 算法会更优一些。
参考:
网络流,二分图与图的匹配 ——Alex_Wei
最大流——OI Wiki
知乎:网络流dinic算法时间复杂度证明?——yukiyama
本文作者:cat-and-code
本文链接:https://www.cnblogs.com/cat-and-code/p/17484680.html
版权声明:本作品采用署名—非商业性使用—相同方式共享 4.0 协议国际版许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步