内容简介:给定一个有向图,有两个特别的点$S$和$T$,每条边都有一个容量限制 ,现询问,从 $ S $ 到 $ T $ 最大可以流多少过去第一眼看过去,跟水厂子往水管里灌水一样简单问题在于这个东西并不好计算
最大流问题
给定一个有向图,有两个特别的点$S$和$T$,每条边都有一个容量限制 ,现询问,从 $ S $ 到 $ T $ 最大可以流多少过去
第一眼看过去,跟水厂子往水管里灌水一样简单
问题在于这个东西并不好计算
有没有办法计算它呢
如果我们每次都枚举我们怎么走的话,时间复杂度是不可估计的
我们有没有办法让我们走过去的流反悔呢?
有,这里就要引入两个概念: 残余网络 && 反向边
残余网络
对于每次我们跑出来的一条流过过后以每条边现能通过的 最大流量 所成的网络图叫 残余网络
反向边
反向边是在给算法一个反悔的机会
如果原图中有一个从 $ u $ 到 $ v $ 的边,则从 $ v $ 到 $ u $ 的边就是这条边的反向边
反向边如何后悔呢?
EK 算法
我们在每一次流过去大小为 $ t $ 一条流时,除了将原有的边上流量减去$ t $之外,也需要将其反向边的流来 加上 $ t $
这样以来,我们就保证以下的两点
- 残余网络中总流量和原图相同
- 在残余网络中走出来的流一定存在
至于怎么解释,咱的看法是,写两边就明白了,看别人的解释之后越看越懵
我们一直寻找可行的流,直到 无法 流到$ t $为止,已经的流过去的和就一定是一个最大流
复杂度: $O(EK(n,m))=O(m^2n)=O(TLE)$
Dinic 算法
EK算法使用bfs寻找流,但bfs寻找流的一些特性决定其不容易实现 当前弧优化 ,以及 多路增广
而且EK算法还会走回路…
这种情况下我们又要引出一个新概念: 分层图
分层图
我们以$ S $为根,可以往下走的边流大于0,然后像树一样记录每个节点的深度(多次访问取第一次)
这样以来我们可以确定以下两点:
- 避免了回路的可能
- 如果存在可行流,则点$ T $深度一定存在
等一下,如果我们搜索的时候都限制只能往当前节点深度+1的点去搜了,我们是不是就可以dfs了?
是的qwq
不过我们先来看一下普通的Dinic吧[Luogu P3376]
#include <cstdio> #include <cstring> inline int Min(int a,int b){return a<b?a:b;} const int N=11000; const int M=110000; int n,m,s,t,u,v,w; // edge start struct edge{ int next,to,val,un; }e[M<<1]; int ehead[N],ecnt; inline void add_edge(int now,int to,int val,int un){ ecnt++; e[ecnt].to=to; e[ecnt].val=val; e[ecnt].next=ehead[now]; e[ecnt].un=ecnt+un; ehead[now]=ecnt; } // edge end namespace Dinic{ int depth[N]; int q[N],head,tail; inline bool bfs(){ memset(depth,0,sizeof(depth)); head=tail=0; q[head]=s; depth[s]=1; while(head<=tail){ for(int i=ehead[q[head]];i;i=e[i].next){ if(e[i].val>0&&depth[e[i].to]==0){ q[++tail]=e[i].to; depth[e[i].to]=depth[q[head]]+1; } } head++; } if(depth[t]==0) return false; else return true; } int dfs(int now,int dist){ if(now==t) return dist; for(int i=ehead[now];i;i=e[i].next){ if(depth[e[i].to]==depth[now]+1&&e[i].val!=0){ int di=dfs(e[i].to,Min(dist,e[i].val)); if(di>0){ e[i].val-=di; e[e[i].un].val+=di; return di; } } } return 0; } inline int dinic(){ int ans=0; while(bfs()){ while(int d=dfs(s,0x7fffffff)) ans+=d; } return ans; } } int main(){ scanf("%d%d%d%d",&n,&m,&s,&t); for(int i=1;i<=m;i++){ scanf("%d%d%d",&u,&v,&w); add_edge(u,v,w,1); add_edge(v,u,0,-1); } printf("%d",Dinic::dinic()); }
好了,我们来讲讲两个优化: 当前弧优化 && 多路增广
当前弧优化
在当前状况下已经有一次从$ u $到$ v $的增广,则不存在第二次从$ u $到$ v $的增广
故此在我们遍历边的时候,我们可以不从 ehead[now]
开始,而是从上一次遍历过的边的下一个开始
namespace Dinic{ int depth[N],rnow[N]; int q[N],head,tail; inline bool bfs(){ memset(depth,0,sizeof(depth)); head=tail=0; q[head]=s; depth[s]=1; while(head<=tail){ for(int i=ehead[q[head]];i;i=e[i].next){ if(e[i].val>0&&depth[e[i].to]==0){ q[++tail]=e[i].to; depth[e[i].to]=depth[q[head]]+1; } } head++; } if(depth[t]==0) return false; else return true; } int dfs(int now,int dist){ if(now==t) return dist; for(int& i=rnow[now];i;i=e[i].next){ if(depth[e[i].to]==depth[now]+1&&e[i].val!=0){ int di=dfs(e[i].to,Min(dist,e[i].val)); if(di>0){ e[i].val-=di; e[e[i].un].val+=di; return di; } } } return 0; } inline int dinic(){ int ans=0; while(bfs()){ for(int i=1;i<=n;i++) rnow[i]=ehead[i]; while(int d=dfs(s,0x7fffffff)) ans+=d; } return ans; } }
多路增广
很多时候在dfs的时候,我们的 dist
还没用完就 return
了,很可惜了有没有
多路增广就可以避免这些问题,我们把剩下的 dist
,继续往下找!
#include <cstdio> #include <cstring> inline int Min(int a,int b){return a<b?a:b;} const int N=11000; const int M=110000; int n,m,s,t,u,v,w; // edge start struct edge{ int next,to,val,un; }e[M<<1]; int ehead[N],ecnt; inline void add_edge(int now,int to,int val,int un){ ecnt++; e[ecnt].to=to; e[ecnt].val=val; e[ecnt].next=ehead[now]; e[ecnt].un=ecnt+un; ehead[now]=ecnt; } // edge end namespace Dinic{ int depth[N],rnow[N]; int q[N],head,tail; inline bool bfs(){ memset(depth,0,sizeof(depth)); head=tail=0; q[head]=s; depth[s]=1; while(head<=tail){ for(int i=ehead[q[head]];i;i=e[i].next){ if(e[i].val>0&&depth[e[i].to]==0){ q[++tail]=e[i].to; depth[e[i].to]=depth[q[head]]+1; } } head++; } if(depth[t]==0) return false; else return true; } int dfs(int now,int dist){ if(now==t) return dist; int fl=0,di; for(int& i=rnow[now];i;i=e[i].next){ if(depth[e[i].to]==depth[now]+1&&e[i].val!=0){ di=dfs(e[i].to,Min(dist,e[i].val)); if(di>0){ e[i].val-=di; e[e[i].un].val+=di; fl+=di; dist-=di; if(!dist) break; } } } return fl; } inline int dinic(){ int ans=0; while(bfs()){ for(int i=1;i<=n;i++) rnow[i]=ehead[i]; while(int d=dfs(s,0x7fffffff)) ans+=d; } return ans; } } int main(){ scanf("%d%d%d%d",&n,&m,&s,&t); for(int i=1;i<=m;i++){ scanf("%d%d%d",&u,&v,&w); add_edge(u,v,w,1); add_edge(v,u,0,-1); } printf("%d",Dinic::dinic()); }
就这些了,那么优化过后的复杂度是什么呢
$O(Dinic(n,m))=O(n^2m)=O(AC)$
还有一个ISAP算法,不在今天的讨论之内,毕竟复杂度和 Dinic
差别不大且常数略大
End
以上所述就是小编给大家介绍的《最大流 && Dinic 算法》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 通俗易懂--决策树算法、随机森林算法讲解(算法+案例)
- 限流算法之漏桶算法、令牌桶算法
- 什么是Paxos算法?Paxos算法是区块链核心算法之一
- 一文读懂对称加密算法、非对称加密算法和Hash算法
- 算法(六):图解贪婪算法
- 算法篇03:排序算法
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
计算统计
Geof H.Givens、Jennifer A.Hoeting / 王兆军、刘民千、邹长亮、杨建峰 / 人民邮电出版社 / 2009-09-01 / 59.00元
随着计算机的快速发展, 数理统计中许多涉及大计算量的有效方法也得到了广泛应用与迅猛发展, 可以说, 计算统计已是统计中一个很重要的研究方向. 本书既包含一些经典的统计计算方法, 如求解非线性方程组的牛顿方法、传统的随机模拟方法等, 又全面地介绍了近些年来发展起来的某些新方法, 如模拟退火算法、基因算法、EM算法、MCMC方法、Bootstrap方法等, 并通过某些实例, 对这些方法的应用进行......一起来看看 《计算统计》 这本书的介绍吧!