EK不够快?再学个Dinic吧

钱逸凡

2018-07-28 13:41:43

Personal

[上次的网络流EK讲解](https://www.luogu.org/blog/ONE-PIECE/wang-lao-liu-di-zong-jie)应该都能听懂吧。 上次讲了EK,有好多人想让我讲一下Dinic,那我就讲解一下Dinic吧~~(顺便看看还能不能再上洛谷日报)~~ # 特别感谢:@ComeIntoPower提出了许多修改建议 ------------ ## 什么是Dinic? 百度百科上的定义是这样的: Dinic算法(又称Dinitz算法)是一个在网络流中计算最大流的强多项式复杂度的算法,设想由以色列(前苏联)的计算机科学家Yefim (Chaim) A. Dinitz在1970年提出。 ~~定义没什么用。~~ 按我的理解,Dinic是这样一种算法: 目的是找最大流,并且很高效。~~(废话)~~ ------------ ## 时间复杂度: 相比于EK的**O(nm^2**)(n为点数,m为边数),DInic可以达到**O(n^2m)**,在稀疏图上,两者相差不大,但在稠密图上(二分匹配之类的)Dinic的优势就很明显了。 对于一些特殊情况, 在具有单位容量的网络中,可以证明每次寻找增广路的复杂度时间为O(m),并且分层次数不超过sqrt(m)和n^(2/3),因此复杂度为O(min(sqrt(m),n^(2/3))m) 在二分图匹配中,复杂度则为O(sqrt(n)m) ~~这些特殊情况的分析是网上找到,我也不会证明,总之Dinic很快,一般不会被卡,除了某些[恶意造数据的题](https://www.luogu.org/problemnew/show/P4722),如果你的Dinic被卡了,可能是图建的不恰当~~ ------------ # Dinic讲解 有了之前EK的讲解,现在看Dinic的讲解应该很好理解,但以防万一,我还是做了图片 不知道你之前学习EK时有没有想过这样的问题:寻找增广路为什么只能一条一条找?可不可以一次找多条?一次找多条增广路可以实现吗?如果能,效率怎么样? 没错,这就是Dinic的主要思想: **多路增广** 具体怎么实现呢? Dinic算法分为两个步骤: 1. bfs分层(在EK中bfs是用于寻找增广路的) 2. dfs增广~~(dfs?EK中貌似没有这玩意啊,确定能高效?)~~ 3. ~~咦!刚才不是说两个步骤吗?~~重复执行1.2.直到图中无增广路为止 什么意思呢? 以下图为例: ![](https://cdn.luogu.com.cn/upload/pic/25635.png) ~~反向边已经加好了,不用讲你也知道反向边是干嘛的吧。~~ 与EK一样,我们仍要通过bfs来判断图中是否还存在增广路,但是DInic算法里的bfs略有不同,这次,我们不用记录路径,而是给每一个点分层,对于任意点i,从s到i每多走过一个点,就让层数多一。 代码与EK略有不同: ``` bool bfs(){ memset(dep,0x3f,sizeof(dep)); memset(inque,0,sizeof(inque)); dep[s]=0; queue<int>q; q.push(s); while(!q.empty()){ int u=q.front(); q.pop(); inque[u]=0; for(int i=head[u];i;i=node[i].next){ int d=node[i].v; if(dep[d]>dep[u]+1&&node[i].val){ dep[d]=dep[u]+1;//注意与EK的区别 if(inque[d]==0){ q.push(d); inque[d]=1; } } } } if(dep[t]!=0x3f3f3f3f)return 1; return 0; }//给增广路上的点分层 ``` 分完层效果是这样的: ![](https://cdn.luogu.com.cn/upload/pic/25636.png) 蓝色的数字是每个点层数 分层有什么用? 有了每个点的层数编号,对任意点u到点d的路径如果有dep[d]==dep[u]+1,我们就可以判断该路径在一条**最短增广路**上。 为什么要找**最短增广路**? 举个极端例子: ![](https://cdn.luogu.com.cn/upload/pic/27822.png) 有了分层,我们就不会选s->1->2->4->5->3->t了 刚才说了的,分完层下一步是dfs增广。 在Dinic中,我们找增广路是用深搜: ~~由于这部分代码应该一看就懂,~~就先放代码,主要讲思想 ``` int dfs(int u,int flow){//u是当前点,low是当前增广路上的最小边权 int rlow=0; if(u==t)return flow;//已经到达t可以用当前路径 for(int i=head[u];i;i=node[i].next){ int d=node[i].v; if(node[i].val&&dep[d]==dep[u]+1){//寻找最短可增广路径 if(rlow=dfs(d,min(flow,node[i].val))){ node[i].val-=rlow; node[i^1].val+=rlow; //正向边加流,反向边减流,思想同EK return rlow; } } } return 0;//无法到达t,无增广路 }//寻找增广路 ``` ~~感觉这玩意应该会很慢,怎么可能比EK快?别急,讲完你就知道了~~ 再放一次刚才的图: ![](https://cdn.luogu.com.cn/upload/pic/25636.png) 分完了层就要开始找增广路了。 比如说,第一次我们找s->1->4->t: ![](https://cdn.luogu.com.cn/upload/pic/25637.png) 路径已经标出来了,由于作者画图时手比较抖~~(主要原因)~~,外加电脑自带的画图软件功能差,略丑。 再仔细看一看图,发现了什么? 还有增广路(显然),标号还可以继续利用!!!那我们可以再执行一次dfs 继续利用第一次bfs出来的标号,再找第二条增广路: s->1->5->t: ![](https://cdn.luogu.com.cn/upload/pic/25638.png) 再找找 竟然还能继续利用标号找第三条增广,再执行dfs s->1->3->t: ![](https://cdn.luogu.com.cn/upload/pic/25641.png) 还有第四条!再执行dfs s->2->3->t: ![](https://cdn.luogu.com.cn/upload/pic/25643.png) 发现了吗?一次bfs我们找了4条增广路! 多么高效,这就是Dinic算法。 思想讲完了就附上我丑陋的代码: ``` //Dinic网络最大流模板 #include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #include<queue> using namespace std; const int inf=1<<30; int dep[10101],head[10101],inque[10101]; int top=1; int maxflow=0; int n,m,s,t; struct Node{ int v; int val; int next; }node[200100]; inline void addedge(int u,int v,int val){ node[++top].v=v; node[top].val=val; node[top].next=head[u]; head[u]=top; } inline int Read(){ int x=0;char c=getchar(); while(c>'9'||c<'0')c=getchar(); while(c>='0'&&c<='9')x=x*10+c-'0',c=getchar(); return x; } bool bfs(){ memset(dep,0x3f,sizeof(dep)); memset(inque,0,sizeof(inque)); dep[s]=0; queue<int>q; q.push(s); while(!q.empty()){ int u=q.front(); q.pop(); inque[u]=0; for(int i=head[u];i;i=node[i].next){ int d=node[i].v; if(dep[d]>dep[u]+1&&node[i].val){ dep[d]=dep[u]+1;//注意与EK的区别 if(inque[d]==0){ q.push(d); inque[d]=1; } } } } if(dep[t]!=0x3f3f3f3f)return 1; return 0; }//给增广路上的点分层 inline int min(int x,int y){ return x<y?x:y; } int dfs(int u,int flow){ int rlow=0; if(u==t)return flow; for(int i=head[u];i;i=node[i].next){ int d=node[i].v; if(node[i].val&&dep[d]==dep[u]+1){ if(rlow=dfs(d,min(flow,node[i].val))){ node[i].val-=rlow; node[i^1].val+=rlow; return rlow; } } } return 0; }//寻找增广路 int Dinic(){ int lowflow; while(bfs()){ while(lowflow=dfs(s,inf))maxflow+=lowflow; } return maxflow; }//Dinic寻找最大流 int main(){ n=Read(),m=Read(),s=Read(),t=Read(); register int i; int u,v,val; for(i=1;i<=m;i++)u=Read(),v=Read(),val=Read(),addedge(u,v,val),addedge(v,u,0); printf("%d",Dinic()); return 0; } ``` 补充说明:Dinic在跑二分图匹配时比匈牙利快很多。 ------------ ## 优化: 上面讲的是普通的Dinic(一般在网上博客找到的版本),其实还可以优化。 其实只要加一点小小的改变,我们就可以在*dfs*时直接实现*多路增广*! 具体怎么实现呢? 我们定义一个变量vis,表示是否能到达t,若能到达t则可以再次dfs,否则重新bfs。(看起来没什么用) 再定义一个变量used,用来表示这个点的流量用了多少。 然后在dfs时我们可以在找到一条增广路时不直接返回,而是改变used的值,如果used还没达到该点流量上限fow,可以继续找别的增广路。 至于代码: ``` int dfs(int u,int flow){ int rlow=0; if(u==t){ vis==1;//可以到达t maxflow+=flow;//其实可以直接在这里累加最大流 return flow; } int used=0;//该点已经使用的流量 for(int i=head[u];i;i=node[i].next){ int d=node[i].v; if(node[i].val&&dep[d]==dep[u]+1){ if(rlow=dfs(d,min(flow-used,node[i].val))){ used+=rlow;//该点使用的流量增加 node[i].val-=rlow; node[i^1].val+=rlow; if(used==flow)break;//该点流量满了,没必要再找了 } } } return used;//返回该点已使用流量 }//寻找增广路 ``` 与之前不同的部分打了注释,请放心食用 当然主函数也要略作修改: ``` int Dinic(){ while(bfs()){ vis=1; while(vis==1){ vis=0; dfs(s,inf); } } return maxflow; }//Dinic寻找最大流 ``` 其实也可以直接这样:(上面的只是为了便于理解) ``` int Dinic(){ while(bfs()){ dfs(s,inf); } return maxflow; }//Dinic寻找最大流 ``` ------------ ## 优化2.0:(传说中的当前弧优化) 我们定义一个数组cur记录当前边(弧)(**功能类比邻接表中的head数组,只是会随着dfs的进行而修改**), 每次我们找过某条边(弧)时,修改cur数组,改成该边(弧)的编号, 那么下次到达该点时,会直接从cur对应的边开始(**也就是说从head到cur中间的那一些边(弧)我们就不走了**)。 有点抽象啊,感觉并不能加快,然而实际上确实快了很多。 在代码中的实现,bfs中: 只需将原来的 ``` memset(dep,0x3f,sizeof(dep)); memset(inque,0,sizeof(inque)); ``` 修改为 ``` for(register int i=0;i<=n;i++)cur[i]=head[i],dep[i]=0x3f3f3f3f,inque[i]=0; //两个memset一个memcpy还不如直接循环 ``` ## 在dfs中 将这一部分: ``` for(int i=head[u];i;i=node[i].next){ int d=node[i].v; if(node[i].val&&dep[d]==dep[u]+1){ if(rlow=dfs(d,min(flow-used,node[i].val))){ used+=rlow; node[i].val-=rlow; node[i^1].val+=rlow; if(used==flow)break; } } } ``` 修改为: ``` for(int i=cur[u];i;i=node[i].next){ cur[u]=i;//修改当前弧 int d=node[i].v; if(node[i].val&&dep[d]==dep[u]+1){ if(rlow=dfs(d,min(flow-uesd,node[i].val))){ used+=rlow; node[i].val-=rlow; node[i^1].val+=rlow; if(used==flow)break; } } } ``` 完整的弧优化代码我就不放了,按上面的改优化一就可以了。 一点点小小的改动就能~~极大程度~~提高你的代码速度 ## 这里还要讲一下当前弧优化的原理: 首先,我们在按顺序dfs时,先被遍历到的边肯定是已经增广过了(或者已经确定无法继续增广了),那么这条边就可以视为“**废边**” 那么下次我们再到达该节点时,就可以直接无视掉所有废边,只走还有用的边,也就是说,每次dfs结束后,下次dfs可以更省时间。 # 感谢观看 ------------ 等等,讲完了吗? 其实还没有, 这种思想也可以运用到*费用流*上,而且肯定比EK快啊。(大多数情况) 第一步当然和EK一样,要跑一遍spfa(要不然怎么保证是最小费用呢) 这里用了一个思想:只要一个从u来的点d满足 dist[d]==dist[u]+node[i].w 就可以表示该点在最短路上(~~应该很好理解,~~和上面的dep[d]==dep[u]+1类似),那我们在增广时就可以走这条边。所以只要把dfs时的条件略作修改即可。 然后与dinic一样,跑dfs进行增广,同时累加最大流,最小费用,同时正向边减流,反向边加流。~~是不是很简单啊,~~直接上代码: ``` int dfs(int u,int flow){ if(u==t){vis[t]=1;maxflow+=flow;return flow;}//可以到达t,加流 int used=0;//该点使用了的流量 vis[u]=1; for(int i=head[u];i;i=node[i].next){ int d=node[i].v; if((vis[d]==0||d==t)&&node[i].val!=0&&dist[d]==dist[u]+node[i].w){ int minflow=dfs(d,min(flow-used,node[i].val)); if(minflow!=0)cost+=node[i].w*minflow,node[i].val-=minflow,node[i^1].val+=minflow,used+=minflow; //可以到达t,加费用,扣流量 if(used==flow)break;//小小的优化 } } return used; } ``` 关于used的解释在上文已经说过了,至于为什么这里的**vis**要用数组,那是因为这是**费用流**,可能存在某条边的费用为0,**如果不给每一个点打访问标记,可能会出现两个点来回跑的情况,**卡死你的程序。 **当然如果t被访问过了,还是可以访问的。** 主函数部分: ``` int mincostmaxflow(){//最小费用最大流 while(spfa()){ vis[t]=1; while(vis[t]){//能到达t就继续 memset(vis,0,sizeof(vis));//由于是数组,要memset dfs(s,inf); } } return maxflow; } ``` 完整代码: ``` #include<iostream> #include<cstring> #include<algorithm> #include<queue> #include<cstdio> using namespace std; const int inf=1<<30; int top=1,head[5010]; int n,m,s,t; int cost,maxflow; int vis[5010];//是否到达过该点 int dist[5010];//到t的单位费用 struct Node{ int val; int v; int next; int w; }node[101010]; inline int Read(){ int x=0; char c=getchar(); while(c>'9'||c<'0')c=getchar(); while(c>='0'&&c<='9')x=x*10+c-'0',c=getchar(); return x; } inline void addedge(int u,int v,int val,int w){ node[++top].v=v; node[top].w=w; node[top].val=val; node[top].next=head[u]; head[u]=top; } bool spfa(){ memset(vis,0,sizeof(vis)); memset(dist,0x3f,sizeof(dist)); dist[s]=0; vis[s]=1; queue<int>q; q.push(s); while(!q.empty()){ int u=q.front(); q.pop(); vis[u]=0; for(int i=head[u];i;i=node[i].next){ int d=node[i].v; if(dist[d]>dist[u]+node[i].w&&node[i].val){ dist[d]=dist[u]+node[i].w; if(vis[d]==0){ q.push(d); vis[d]=1; } } } } return dist[t]!=0x3f3f3f3f; }//spfa同EK inline int min(int x,int y){ return x<y?x:y; } int dfs(int u,int flow){ if(u==t){vis[t]=1;maxflow+=low;return flow;}//可以到达t,加流 int used=0;//该条路径可用流量 vis[u]=1; for(int i=head[u];i;i=node[i].next){ int d=node[i].v; if((vis[d]==0||d==t)&&node[i].val!=0&&dist[d]==dist[u]+node[i].w){ int minflow=dfs(d,min(flow-used,node[i].val)); if(minflow!=0)cost+=node[i].w*minflow,node[i].val-=minflow,node[i^1].val+=minflow,used+=minflow; //可以到达t,加费用,扣流量 if(used==flow)break; } } return used; } int mincostmaxflow(){ while(spfa()){ vis[t]=1; while(vis[t]){ memset(vis,0,sizeof(vis)); dfs(s,inf); } } return maxflow; } int main(){ n=Read(),m=Read(),s=Read(),t=Read(); int u,v,w,val; register int i; for(i=1;i<=m;i++){ u=Read(),v=Read(),val=Read(),w=Read(); addedge(u,v,val,w); addedge(v,u,0,-w); } mincostmaxflow(); printf("%d %d",maxflow,cost); return 0; } ``` ### 总结:在图稀疏时此算法与EK差不多,但当图较稠密时就明显比EK优,而且代码也没比EK长多少(只多了一个dfs),根据情况决定使用哪个吧。 究竟能快到什么程度呢? 我做[洛谷P4003](https://www.luogu.org/problemnew/show/P4003) 时两种都用了一次~~(主要是第一次EK没吸氧T了)~~ 可以对比一下时间: [EK(还吸了氧气的)](https://www.luogu.org/record/show?rid=9615262) [这种算法(没吸氧气)](https://www.luogu.org/record/show?rid=9874702) ## 感谢观看 ## 欢迎各位dalao提出修改建议 [ISAP和HLPP已经写好了(作者快累死了)](https://www.luogu.org/blog/ONE-PIECE/jiu-ji-di-zui-tai-liu-suan-fa-isap-yu-hlpp)