*【学习笔记】(4) 网络流
1.算法简介
网络
一个网络
流
如果把网络想象成一个自来水管道网络,那流就是其中流动的水。每条边上的流不能超过它的容量,并且对于除了源点和汇点外的所有点(即中继点),流入的流量都等于流出的流量。
设
- 容量限制:对于每条边,流经该边的流量不得超过该边的容量,即,
- 斜对称性:每条边的流量与其相反边的流量之和为
,即 - 流守恒性:从源点流出的流量等于汇点流入的流量,即
那么
一般而言也可以把网络流理解为整个图的流量。而这个流量必满足上述三个性质。
注:流函数的完整定义为
如下图,就是一个网络,在图中,有向边用 "
2.最大流
网络流中最常见的问题就是网络最大流。假定从源点流出的流量足够多,求能够流入汇点的最大流量(即使
Edmond-Karp算法
若一条从源点
在每轮寻找增广路的过程中,Edmond-Karp算法只考虑图中所有
但是仅仅这样可能会出错。如下图。
如果我们首先找到了
现在已经找不到任何增广路了,最终求得最大流是1。但是,很明显,如果我们分别走
这时候,开始建的反向边就起作用了。
如果我们仍然选择
这时我们可以另外找到一条增广路:
其实可以把反向边理解成一种撤销,走反向边就意味着撤回上次流经正向边的若干流量,这也合理解释了为什么扣除正向边容量时要给反向边加上相应的容量:反向边的容量意味着可以撤回的量,本质上就是一种反悔贪心。
加入了反向边这种反悔机制后,我们就可以保证,当找不到增广路的时候,流到汇点的流量就是最大流。
Edmond-Karp算法时间复杂度为
关于增广有一个常用技巧:成对变换。网络流建图一般使用链式前向星,我们将每条边与它的反向边按编号连续存储,编号分别记为
#include<cstdio> #include<queue> #define M 10005 #define INF 0x3f3f3f3f3f3f3f3f #define N 205 #define LL long long using namespace std; int n,m,s,t,tot=1; LL maxflow; LL Head[N],edge[M],to[M],Next[M]; LL incf[N],pre[N]; void add(int u,int v,int w){ to[++tot]=v,edge[tot]=w,Next[tot]=Head[u],Head[u]=tot; to[++tot]=u,edge[tot]=0,Next[tot]=Head[v],Head[v]=tot; //反边 } bool bfs(){ // 寻找增广路 int vis[N]={0}; queue<int> q; q.push(s); vis[s]=1; incf[s]=INF; while(!q.empty()){ int x=q.front(); q.pop(); for(int i=Head[x];i;i=Next[i]){ if(!edge[i]) continue; int y=to[i]; if(vis[y]) continue; incf[y]=min(edge[i],incf[x]); pre[y]=i; q.push(y); vis[y]=1; if(y==t) return true; } } return false; } void update(){ int x=t; while(x!=s){ int i=pre[x]; edge[i]-=incf[t]; edge[i^1]+=incf[t]; x=to[i^1]; } maxflow+=incf[t]; } int main(){ scanf("%d%d%d%d",&n,&m,&s,&t); for(int i=1;i<=m;i++){ int u,v,w; scanf("%d%d%d",&u,&v,&w); add(u,v,w); } while(bfs()) update(); printf("%lld\n",maxflow); return 0; }
复杂度证明
在任意时刻,网络中所有节点以及剩余容量大于0的边构成的子图被称为残余网络。
为证明 EK 的时间复杂度,我们需要这样一条引理:每次增广后残量网络上
考虑反证法,假设存在节点
分类讨论
如果
如果
引理证毕。
接下来证明 EK 算法的复杂度。
不妨设某次增广的增广路为
对于关键边
而增广后,
由引理我们知道
故有
所以每次出现至少会使得最短距离
而每次增广的 BFS 的复杂度为
Dinic算法
因为 EK 算法每轮遍历整张图只会找出一条最短路,所以还有进一步的优化空间。
最常用的网络流算法是Dinic算法。作为EK算法的优化,它选择了先用BFS分层,再用DFS寻找。它的时间复杂度上界是
我们可以使用多路增广节省很多花在重复路线上的时间:在某点DFS找到一条增广路后,如果还剩下多余的流量未用,继续在该点DFS尝试找到更多增广路。
此外还有当前弧优化。因为在Dinic算法中,一条边增广一次后就不会再次增广了,所以下次增广时不需要再考虑这条边。我们把head数组复制一份,但不断更新增广的起点。
#include<cstdio> #include<queue> #include<cstring> #define M 10005 #define INF 0x3f3f3f3f3f3f3f3f #define N 205 #define LL long long using namespace std; int n,m,s,t,tot=1;//attention!!! 好多时候我出错都是因为这里 LL maxflow,now[N],d[N]; LL Head[N],edge[M],to[M],Next[M]; void add(int u,int v,int w){ to[++tot]=v,edge[tot]=w,Next[tot]=Head[u],Head[u]=tot; to[++tot]=u,edge[tot]=0,Next[tot]=Head[v],Head[v]=tot; } bool bfs(){//分层 memset(d,0,sizeof(d)); while(!q.empty()) q.pop(); q.push(s),d[s]=1;now[s]=Head[s]; while(!q.empty()){ int x=q.front();q.pop(); for(int i=Head[x];i;i=Next[i]){ int y=to[i];if(d[y]||!edge[i]) continue;//容量为 0了就不要了 now[y]=Head[y]; d[y]=d[x]+1; if(y==t) return 1; q.push(y); } } return 0; } int dinic(int x,LL flow){ if(x==t||!flow) return flow; LL rest=flow; for(int i=now[x];i;i=Next[i]){ now[x]=i;//当前弧优化 int y=to[i];if(d[y]!=d[x]+1||!edge[i]) continue; int k=dinic(y,min(rest,edge[i])); if(!k) d[y]=0; rest-=k,edge[i]-=k,edge[i^1]+=k; if(!rest) return flow; } return flow-rest; } int main(){ scanf("%d%d%d%d",&n,&m,&s,&t); for(int i=1;i<=m;i++){ int u,v,w; scanf("%d%d%d",&u,&v,&w); add(u,v,w); } LL flow=0; while(bfs()) while(flow=dinic(s,INF)) maxflow+=flow; printf("%lld\n",maxflow); return 0; }
需要注意的是,当前弧优化必须放在循环内,即每次都更新一下,防止走出去了又走回来而
复杂度证明
在证明 EK 的时间复杂度时,我们使用了一个引理,就是
到每个节点的最短路单调不减。因为 dinic 蕴含 EK,所以该引理仍然成立。
我们现在尝试证明对于 dinic 的一次增广,
反证法,假设存在一次增广使得
考察 增广后 的一条从
由引理,增广前
若对于所有
因此,存在
又因为增广后
矛盾,证毕。
- 上述证明中我们没有用到
的任何性质,故同理可证一轮增广从 S 到每个点的最短路增加,前提是 在增广前可达该点。
这样如果最短路每次最少增加 1,那么增广轮数最多为
BFS 时间
由于高度标号加一的判断条件限制,DFS只能沿着高度递增(+1)的方向从
在分层图中,每次DFS增广的结果使得一条高度递增方向上的关键边
一次增广至少令一条高度递增的关键边并删除,高度递增边数量不大于总边数
所以一轮增广的时间复杂度为
所以 dinic 的总时间复杂度为
2.1 例题:
1. P3376 【模板】网络最大流
模板题。
#include<cstdio> #include<queue> #include<cstring> #define M 10005 #define INF 0x3f3f3f3f3f3f3f3f #define N 205 #define LL long long using namespace std; int n,m,s,t,tot=1; LL maxflow,now[N],d[N]; LL Head[N],edge[M],to[M],Next[M]; void add(int u,int v,int w){ to[++tot]=v,edge[tot]=w,Next[tot]=Head[u],Head[u]=tot; to[++tot]=u,edge[tot]=0,Next[tot]=Head[v],Head[v]=tot; } bool bfs(){ memset(d,0,sizeof(d)); queue<int> q; q.push(s); d[s]=1; now[s]=Head[s]; while(!q.empty()){ int x=q.front(); q.pop(); for(int i=Head[x];i;i=Next[i]){ int y=to[i]; if(!edge[i]||d[y]) continue; now[y]=Head[y]; q.push(y); d[y]=d[x]+1; if(y==t) return true; } } return false; } LL dinic(int x,LL flow){ if(x==t) return flow; LL rest=flow,k,i; for(i=now[x];i&&rest;i=Next[i]){ int y=to[i]; now[x]=i; if(edge[i]&&d[y]==d[x]+1){ k=dinic(y,min(rest,edge[i])); if(!k) d[y]=0; edge[i]-=k; edge[i^1]+=k; rest-=k; } } return flow-rest; } int main(){ scanf("%d%d%d%d",&n,&m,&s,&t); for(int i=1;i<=m;i++){ int u,v,w; scanf("%d%d%d",&u,&v,&w); add(u,v,w); } LL flow=0; while(bfs()) while(flow=dinic(s,INF)) maxflow+=flow; printf("%lld\n",maxflow); return 0; }
各种模型
1.
参考资料:《算法竞赛进阶指南》,算法学习笔记(28): 网络流。
部分证明引自: 网络流,二分图与图的匹配 ,https://www.luogu.com.cn/blog/Link-Cut-Treeeee/qian-tan-wang-lao-liu-jian-mu-di-ji-ji-yin-qiao
本文作者:南风未起
本文链接:https://www.cnblogs.com/jiangchen4122/p/17417621.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步