最大流算法 SAP+GAP
2015-01-10 08:59
381 查看
脑补系列
准备好基础知识,我们就要开始学习高大上的最大流啦~ :D (这里有一只2b青年-_-||)
虽然求最大流的算法有很多,如Dinic、SAP、EK……但归根结底就一个思路:不停找增广路径直到不存在增广路。SAP简洁易懂,写成递归后易于调试,并且据说目前的合法网络流题目的数据都卡不了SAP,所以在此只介绍SAP算法。
SAP(Shortest Augmenting Paths),最短增广路,俗称标号法,利用标号寻找增广路,使得每次搜索的复杂度尽可能小,从而达到降低时间复杂度的目的。标号可以理解为结点当前到汇点的最小距离,注意是当前的最短距离而不是一直等于实际最短距离,在下文中会提到为何这样做。并且标号必定满足一个性质:结点i的标号不可能超过i到汇点的最远距离。拿源点举个例子,当S(源点)的标号大于等于N(结点总数)时,表示从S到T至少要走N条边,但是从S到T最远路径即经过每个结点的路径也只等于N-1,所以必定会重复经过某些结点,但这有与网络流的基本定义相悖,所以如果S到T的标号>=N,那么从S到T就不存在可行的增广路。由此推到一般情况,如上文提到的,结点i的标号不可能超过i到汇点的最远距离。
那么,SAP是如何工作呢?
算法基本框架:
1.定义结点的标号为到汇点的最短距离(有的文章写的是到源点的距离,都是一个意思)。
2.每次沿可行边寻找增广路,大多数情况下都采用DFS寻找增广路。可行边定义为:{(now,next)|h[now]=h[next]+1},其中h[i]为结点i的标号。这样解释一下吧:假设有这样两个点i,j,h[i]=4,h[j]=3,并且从i到j有一条边,那么这条边就是一条可行边。
3.找到增广路后,将路径上所有边的流量更新;遍历完当前结点的可行边后以后更新当前结点的标号为min{h[next]|Flow(now,next)>0}+1,使下次再搜的时候有路可走。做完当前结点是个什么情况呢?首先我们要理解更新标号的目的。标号如果需要更新,说明在当前的标号下已经没有增广路可以继续走,这时更新标号就可以使得我们有继续向下走的可能,并且每次找的都是能走到的点中标号最小的那个点,这样也使得每次搜索长度最小(Q:这个不是说过了么?A:再说一遍不是加深理解么~)。怎样理解呢?由于接下来的模拟标号过程图有点多,所以另开一篇讲解(已经理解标号过程的就不需要看了):猛戳这里→_→/article/2137872.html
4.图中不存在增广路后即退出程序,此时得到的流量值就是最大流。
☆GAP优化☆:十二个字形容:简洁易懂,三行代码,极大优化。由于可行边定义为:{(now,next)|h[now]=h[next]+1},所以若标号出现“断层”即有的标号对应的顶点个数为0,则说明剩余图中不存在增广路,此时便可以直接退出,降低了无效搜索。举个栗子:若结点标号为3的结点个数为0,而标号为4的结点和标号为2的结点都>0,那么在搜索至任意一个标号为4的结点时,便无法再继续往下搜索(因为4要搜到3然后在搜到2是不是~),说明图中就不存在增广路。此时我们可以以将h[1]=n形式来变相地直接结束搜索。GAP优化的代码总共就3行,非常好写也很好懂。
有人问重边怎么办?重边也是照样做,不需要特殊处理。
PS:
在代码line72中出现了i^1,由于本文主要针对初学者,所以可能有些不理解。在找到一条增广路后需要更新正反边的流量,用邻接矩阵的同学就笑笑不说话,而代码以数组模拟链表的方式建图,就显得没有那么方便了。如何解决呢?于是我们使用了一个位运算"^"即异或。对于任意一个数K,若K为奇数,则K^1=K+1,若K为偶数,则K^1=K-1。看一下代码中init过程不难发现正向边的编号都是偶数,反向边的编号都是奇数。所以对于任意一条编号为K的正/反向边,所对应的反/正向边的编号就等于K^1,这样就可以将正反向边一样处理,使这一段代码变得非常简洁。
时间复杂度:O(M*N^2),加入GAP优化后可成倍提升效率。
具体分析见代码注释:有疑惑或没写清楚的地方欢迎指出~
准备好基础知识,我们就要开始学习高大上的最大流啦~ :D (这里有一只2b青年-_-||)
虽然求最大流的算法有很多,如Dinic、SAP、EK……但归根结底就一个思路:不停找增广路径直到不存在增广路。SAP简洁易懂,写成递归后易于调试,并且据说目前的合法网络流题目的数据都卡不了SAP,所以在此只介绍SAP算法。
SAP(Shortest Augmenting Paths),最短增广路,俗称标号法,利用标号寻找增广路,使得每次搜索的复杂度尽可能小,从而达到降低时间复杂度的目的。标号可以理解为结点当前到汇点的最小距离,注意是当前的最短距离而不是一直等于实际最短距离,在下文中会提到为何这样做。并且标号必定满足一个性质:结点i的标号不可能超过i到汇点的最远距离。拿源点举个例子,当S(源点)的标号大于等于N(结点总数)时,表示从S到T至少要走N条边,但是从S到T最远路径即经过每个结点的路径也只等于N-1,所以必定会重复经过某些结点,但这有与网络流的基本定义相悖,所以如果S到T的标号>=N,那么从S到T就不存在可行的增广路。由此推到一般情况,如上文提到的,结点i的标号不可能超过i到汇点的最远距离。
那么,SAP是如何工作呢?
算法基本框架:
1.定义结点的标号为到汇点的最短距离(有的文章写的是到源点的距离,都是一个意思)。
2.每次沿可行边寻找增广路,大多数情况下都采用DFS寻找增广路。可行边定义为:{(now,next)|h[now]=h[next]+1},其中h[i]为结点i的标号。这样解释一下吧:假设有这样两个点i,j,h[i]=4,h[j]=3,并且从i到j有一条边,那么这条边就是一条可行边。
3.找到增广路后,将路径上所有边的流量更新;遍历完当前结点的可行边后以后更新当前结点的标号为min{h[next]|Flow(now,next)>0}+1,使下次再搜的时候有路可走。做完当前结点是个什么情况呢?首先我们要理解更新标号的目的。标号如果需要更新,说明在当前的标号下已经没有增广路可以继续走,这时更新标号就可以使得我们有继续向下走的可能,并且每次找的都是能走到的点中标号最小的那个点,这样也使得每次搜索长度最小(Q:这个不是说过了么?A:再说一遍不是加深理解么~)。怎样理解呢?由于接下来的模拟标号过程图有点多,所以另开一篇讲解(已经理解标号过程的就不需要看了):猛戳这里→_→/article/2137872.html
4.图中不存在增广路后即退出程序,此时得到的流量值就是最大流。
☆GAP优化☆:十二个字形容:简洁易懂,三行代码,极大优化。由于可行边定义为:{(now,next)|h[now]=h[next]+1},所以若标号出现“断层”即有的标号对应的顶点个数为0,则说明剩余图中不存在增广路,此时便可以直接退出,降低了无效搜索。举个栗子:若结点标号为3的结点个数为0,而标号为4的结点和标号为2的结点都>0,那么在搜索至任意一个标号为4的结点时,便无法再继续往下搜索(因为4要搜到3然后在搜到2是不是~),说明图中就不存在增广路。此时我们可以以将h[1]=n形式来变相地直接结束搜索。GAP优化的代码总共就3行,非常好写也很好懂。
有人问重边怎么办?重边也是照样做,不需要特殊处理。
PS:
在代码line72中出现了i^1,由于本文主要针对初学者,所以可能有些不理解。在找到一条增广路后需要更新正反边的流量,用邻接矩阵的同学就笑笑不说话,而代码以数组模拟链表的方式建图,就显得没有那么方便了。如何解决呢?于是我们使用了一个位运算"^"即异或。对于任意一个数K,若K为奇数,则K^1=K+1,若K为偶数,则K^1=K-1。看一下代码中init过程不难发现正向边的编号都是偶数,反向边的编号都是奇数。所以对于任意一条编号为K的正/反向边,所对应的反/正向边的编号就等于K^1,这样就可以将正反向边一样处理,使这一段代码变得非常简洁。
时间复杂度:O(M*N^2),加入GAP优化后可成倍提升效率。
#include <stdio.h> #include <algorithm> #include <string.h> #include <iostream> using namespace std; #define read freopen("a.in","r",stdin) #define write freopen("a.out","w",stdout) const int maxn=5005; int head[maxn*100],next[maxn*100],l[maxn*100],node[maxn*100]; int n,m,flow,now,h[maxn],vh[maxn],tot,aug; bool found; //结点i的标号为h[i](用于SAP) 标号为j的点数有vh[j]个(用于GAP) void add(int x,int y,int z){ tot++; node[tot]=y; next[tot]=head[x]; head[x]=tot; l[tot]=z; } void init(){ int x,y,z; //read;write; cin>>m>>n;tot=-1; //见line16 memset(head,-1,sizeof head); //注意不能初始化为0 因为第一条边的编号为0 for (int i=1;i<=m;i++){ cin>>x>>y>>z; add(x,y,z); add(y,x,0); //反向边 } memset(h,0,sizeof h); memset(vh,0,sizeof vh); vh[0]=n; } void find(int t){ int p,augc,minh; if (t==n){ found=1; flow+=aug; return ; } augc=aug; //因为aug是个全局变量 对最大流存个备份 见line58 minh=n-1; //为什么要这样初始化而不是等于0x7fffff? 自己YY去-_-|| for (int i=head[t];i!=-1;i=next[i]) if (l[i]>0){ //首先要满足这条边上还有剩余流量 if (h[t]==h[node[i]]+1){ //判断可行边 aug=min(aug,l[i]); //一条路径上的最大流=剩余流量最小的边的流量 find(node[i]); if (h[1]>=n) return ; //无可行增广路 if (found){ p=i;//只需要找到一条增广路就行 所以记录一下点就直接退出循环 不需要再开一个记录路径的数组 break; } aug=augc; } minh=min(minh,h[node[i]]); //记录最小标号 } if (!found){ vh[h[t]]--; //GAP if (vh[h[t]]==0) h[1]=n;//GAP 标号出现断层说明不存在增广路,为什么要将h[1]=n见line 53 h[t]=minh+1; //重标号 vh[h[t]]++; //GAP } else{ l[p]-=aug; //找到增广路以后对路径上的边的流量进行更新 l[p^1]+=aug; } } int main(){ init(); while (h[1]<n){ found=0; //记得每次都要初始化 aug=0x7fffff; find(1); } cout<<flow<<endl; return 0; }
具体分析见代码注释:有疑惑或没写清楚的地方欢迎指出~
相关文章推荐
- 网络流算法总结 Edmond-Krap + SAP(GAP优化)+Dinic
- 最大流算法的选择:Dinic还是SAP?
- 最大流算法----(SAP 和 EK)
- SAP(最短增广路算法) 最大流模板
- 网络流算法总结 Edmond-Krap + SAP(GAP优化)+Dinic
- 网络流——基础,Dinic和Sap(Gap优化)算法
- poj-1273-Drainage Ditches-一般预流推进算法-最高标号预流推进算法-sap+gap优化
- M - Escape - HDU 3605 - (缩点+最大流SAP)
- 网络流之 最短增广路算法模板(SAP)
- [网络流]最大流算法 Dinic
- 最大流算法_一般曾广路算法Ford
- HDU 3468 Treasure Hunting (最大流SAP)经典(看似广搜实则最大流)
- POJ1459 Power Network (SAP+GAP优化)
- bzoj3085: 反质数加强版SAPGAP
- 最大流学习笔记(6)-前置重贴标签算法二
- 34:E ·Power Network(最大流基础算法篇)
- 34:E ·Power Network(最大流算法-----压入重标法篇)
- SAP 算法 求最大流 poj 3189 pku
- hiho一下 第115周:网络流一•Ford-Fulkerson算法 (Edmond-Karp,Dinic,SAP)
- 最大流算法 Dinic HDU 1532