网络流(Dinic算法)学习笔记

hkr04

2018-10-18 15:58:06

Personal

二分图匹配里的好多题解都是网络流啊……水完二分图匹配的dfs版我决定学习一下垂涎已久的网络流(雾) 参考博客:[EK不够快? 再学个Dinic吧 by 钱逸凡](https://www.luogu.org/blog/ONE-PIECE/wang-lao-liu-jiang-xie-zhi-dinic) [Dinic算法 by SYCstudio](https://www.cnblogs.com/SYCstudio/p/7260613.html) 参考书籍:刘汝佳《算法竞赛入门经典第二版》 网络流:在一个**有向图**上选择一个**源点s**、一个**汇点t**。源点只流出,汇点只流进。同时,一条边(u,v)有的**流量**记为**f(u,v)**,也有**最大流量**称为**容量**,记为**c(u,v)**。(若该边不存在,则c(u,v)==0)。一条边上的剩余流量(没有用完的)称为**残量**,即 容量-流量 。除了源点和汇点,同一时间所有的边的流量应该是相等的 基本性质: 1. $f(u,v)\le c(u,v)$**(容量限制)** 2. 对于任何一个不是源点或汇点的点u,总有$$\sum_{p\in E}f(p,u)=\sum_{q\in E}f(u,q)$$ **因为入流和出流相等(斜对称性)** 3. 对于任何一条有向边(u,v),总有$f(u,v)=-f(v,u)$为什么呢?因为把3个物品从u运送到v后再把3个物品送回u是没有意义的,流量相当于0 >这样规定就好比“把3个物品从u运送v”等价于“把-3个物品从v运送到u”一样 ——紫书 最大流经典例题:物资运送 **最大流问题**的目标是把最多的物品从s运送到t,而其他结点都只是中转,因此对于除了结点s和t以外的任意结点u,**(流量平衡)**$$\sum_{(u,v)\in E}f(u,v)=0$$(这些f中有些是负数) 从s运送出来的物品数目等于到达t的物品数目,而这正是此处最大化的目标 ——紫书 对于增广路: 算法思想:从零流(所有边的流量均为0)开始不断增加流量,保持每次增加流量后都满足以上性质。计算每条边上的殘量,得到**殘量网络**(注意殘量网络中的边数,可能达到原图中边数的两倍,因为加上了反边)。增广路算法基于:殘量网络中任何一条从s到t的有向道路都对应一条原图中的增广路——只要求出该道路中所有殘量的最小值d,把对应的所有边上的流量减去d,在答案里加上d即可,这个过程称为**增广** **显然只要殘量网络中存在增广路,流量就可以增大;反之,如果不存在增广路,流量就已经最大。(增广路定理)** 但是如果一条一条地找出增广路,万一有一些极(毒)端(瘤)数据(比如几条相邻的边容量相差特别大),这个时间复杂度就是无法承受的。Dinic算法的高效之处在于它能够同时找出几条增广路:运用**分层图** 具体怎么实现呢? >Dinic算法分为两个步骤: 1. bfs分层 2. dfs增广 3. 重复执行1.2直到图中无增广路为止 > by 钱逸凡 一些小细节: 1. 存图的时候要从偶数开始存,同时存正向边和反向边(这样就可以保证正向边编号全为偶数,反向边编号为i^1(奇偶性相反)) 2. 反向边怎么用?因为找到的增广路不一定是最优的,反边给你“反悔”的机会。如果这条边边权为0,那么往回走的时候并不会对流量有所影响(走不回去)。所以一开始反边的边权应该存0,当正向边减去**所有残量的最小量d**(此时这个值已经被加入到了答案)的时候,反向边应该加上d(因为对于源点和汇点来说中间流量的这些变化都是无差别的,为了保证反向正向相加得原边权,不改变原本的条件就得这么做) 来吧!上代码! ```cpp #include <cstdio> #include <queue> #include <cstring> const int maxn=10000+10; const int maxm=100000+10; const int INF=0x7fffffff; int head[maxn],nxt[maxm*2];//因为还要存反向边,所以要开两倍 int to[maxm*2],val[maxm*2]; int dep[maxn]; bool inque[maxn]; int n,m,s,t; int tot=1,maxflow=0; inline int min(int x,int y) {return x<y?x:y;} void add(int u,int v,int w)//链式前向星存图 { nxt[++tot]=head[u]; to[tot]=v; val[tot]=w; head[u]=tot; } void AddEdge() { int u,v,w; while(m--) { scanf("%d%d%d",&u,&v,&w); add(u, v, w);//正边 add(v, u, 0);//反边 } } bool bfs()//给增广路上的点分层 { memset(dep, 0x3f3f3f3f, sizeof(dep)); memset(inque, 0, sizeof(inque)); dep[s]=0;//源点深度当然为0 std::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=nxt[i]) { int v=to[i];//通向的点 if (val[i]&&dep[v]>dep[u]+1)//如果容量不为0且在u点之前还没有被搜到 { dep[v]=dep[u]+1; if (!inque[v])//如果点v不在当前队伍中 { q.push(v); inque[v]=1; } } } } if (dep[t]!=0x3f3f3f3f)//只要汇点被搜到了,就还有增广路 return 1; return 0; } int dfs(int pos,int low)//当前位置和最小残量,dfs用于寻找增广路 { int rlow=0; if (pos==t)//到达汇点 { maxflow+=low;//直接将最大流更新 return low;//返回最小残量(当它为0时说明没有增广路了) } int used=0;//该点已经使用了的流量 for (int i=head[pos];i;i=nxt[i]) { int v=to[i]; if (val[i]&&dep[v]==dep[pos]+1)//小优化:仅当v在当前位置的下一层中才进行查找是否有增广路 if (rlow=dfs(v, min(low-used, val[i])))//注意这里的min { used+=rlow;//该点使用的流量增加 val[i]-=rlow; val[i^1]+=rlow; if (used==low)//该点已达最大流量,不用继续找了 break; } } return used;//返回该点已使用流量 } int Dinic() { while(bfs())//如果还有增广路就继续dfs dfs(s, INF); return maxflow; } int main() { scanf("%d%d%d%d",&n,&m,&s,&t); AddEdge(); printf("%d\n",Dinic()); return 0; } ```