网络流入门题,第一次用ISAP写网络流的题~~~~~
Improved SAP(ISAP)算法
ISAP字面意思是改良的最短增广路算法。关于ISAP,一位叫 DD_engi 的神牛讲非常清楚,引用一下:
SAP算法(by dd_engi):求最大流有一种经典的算法,就是每次找增广路时用BFS找,保证找到的增广路是弧数最少的,也就是所谓的 Edmonds-Karp 算法。可以证明的是在使用最短路增广时增广过程不超过 V * E次,每次 BFS 的时间都是O(E),所以 Edmonds-Karp 的时间复杂度就是O(V * E^2)。
如果能让每次寻找增广路时的时间复杂度降下来,那么就能提高算法效率了,使用距离标号的最短增广路算法就是这样的。所谓距离标号,就是某个点到汇点的最少的弧的数量(另外一种距离标号是从源点到该点的最少的弧的数量,本质上没什么区别)。设点 i 的标号为D[i],那么如果将满足D[i] = D[j] + 1的弧(i,j))叫做允许弧,且增广时只走允许弧,那么就可以达到“怎么走都是最短路”的效果。每个点的初始标号可以在一开始用一次从汇点沿所有反向边的BFS求出,实践中可以初始设全部点的距离标号为0,问题就是如何在增广过程中维护这个距离标号。
维护距离标号的方法是这样的:当找增广路过程中发现某点出发没有允许弧时,将这个点的距离标号设为由它出发的所有弧的终点的距离标号的最小值加一。这种维护距离标号的方法的正确性我就不证了。由于距离标号的存在,由于“怎么走都是最短路”,所以就可以采用DFS找增广路,用一个栈保存当前路径的弧即可。当某个点的距离标号被改变时,栈中指向它的那条弧肯定已经不是允许弧了,所以就让它出栈,并继续用栈顶的弧的端点增广。为了使每次找增广路的时间变成均摊O(V),还有一个重要的优化是对于每个点保存“当前弧”:初始时当前弧是邻接表的第一条弧;在邻接表中查找时从当前弧开始查找,找到了一条允许弧,就把这条弧设为当前弧;改变距离标号时,把当前弧重新设为邻接表的第一条弧,还有一种在常数上有所优化的写法是改变距离标号时把当前弧设为那条提供了最小标号的弧。当前弧的写法之所以正确就在于任何时候我们都能保证在邻接表中当前弧的前面肯定不存在允许弧。
还有一个常数优化是在每次找到路径并增广完毕之后不要将路径中所有的顶点退栈,而是只将瓶颈边以及之后的边退栈,这是借鉴了Dinic算法的思想。注意任何时候待增广的“当前点”都应该是栈顶的点的终点。这的确只是一个常数优化,由于当前边结构的存在,我们肯定可以在O(n)的时间内复原路径中瓶颈边之前的所有边。
优化:
1.邻接表优化:
如果顶点多的话,往往N^2存不下,这时候就要存边:
存每条边的出发点,终止点和价值,然后排序一下,再记录每个出发点的位置。以后要调用从出发点出发的边时候,只需要从记录的位置开始找即可(其实可以用链表)。优点是时间加快空间节省,缺点是编程复杂度将变大,所以在题目允许的情况下,建议使用邻接矩阵。
2.GAP优化:
如果一次重标号时,出现距离断层,则可以证明ST无可行流,此时则可以直接退出算法。
3.当前弧优化:
为了使每次找增广路的时间变成均摊O(V),还有一个重要的优化是对于每个点保存“当前弧”:初始时当前弧是邻接表的第一条弧;在邻接表中查找时从当前弧开始查找,找到了一条允许弧,就把这条弧设为当前弧;改变距离标号时,把当前弧重新设为邻接表的第一条弧。
另外,ISAP简化的描述是:程序开始时用一个反向 BFS 初始化所有顶点的距离标号,之后从源点开始,进行如下三种操作:(1)当前顶点 i 为终点时增广 (2) 当前顶点有满足 dist[i] = dist[j] + 1 的出弧时前进 (3) 当前顶点无满足条件的出弧时重标号并回退一步。整个循环当源点 s 的距离标号 dist[s] >= n 时结束。对 i 点的重标号操作可概括为 dist[i] = 1 + min{dist[j] : (i,j)属于残量网络Gf}。
EK算法每次都要重新寻找增广路,寻找过程只受残余网络的影响,如果改变残余网络,则增广路的寻找也会随之改变;SAP算法预处理出了增广路的寻找大致路径,若中途改变残余网络,则此算法将重新进行。EK处理在运算过程中需要不断加边的最大流比SAP更有优势。
AC代码:
#include<iostream> #include<algorithm> #include<queue> #include<string.h> #include<cstdio> #define M 16 #define N 2001 #define inf 0xfffff using namespace std; typedef struct str { int v; int w; int next; }Edge; Edge po[N]; int head[M],lev[M],gap[M],flow[N],pre[N],cur[M]; int tot; void init() { memset(head,-1,sizeof(head)); memset(lev,-1,sizeof(lev)); memset(gap,0,sizeof(gap)); memset(flow,0,sizeof(flow)); tot=0; } inline int Read() { char ch=getchar(); while(ch<'0'||ch>'9') ch=getchar(); int data=0; do { data=data*10+ch-'0'; ch=getchar(); }while(ch>='0'&&ch<='9'); return data; } void add(int a,int b,int c) { po[tot].v=b; po[tot].w=c; po[tot].next=head[a]; head[a]=tot++; } void bfs(int t) { queue<int>Q; Q.push(t); lev[t]=0; while(!Q.empty()) { int v=Q.front(); Q.pop(); gap[lev[v]]++; for(int i=head[v];i!=-1;i=po[i].next) { if(lev[po[i].v]==-1) { lev[po[i].v]=lev[v]+1; Q.push(po[i].v); } } } } int sap(int s,int t) { int maxflow=0,low=inf,now=s; bfs(t);//对图中的所有点进行初始化标号 memcpy(cur,head,sizeof(head)); while(lev[now]<t) { int &i=cur[now]; for(;i!=-1;i=po[i].next) { if(po[i].w>flow[i]&&lev[now]==lev[po[i].v]+1) { low=min(low,po[i].w-flow[i]);//更新增广流 pre[po[i].v]=i;//记录该点所在的前向边 now=po[i].v;//找到可行弧,更新当前点 if(now==t)//找到一条从源点到汇点的路径,更新该路径中边的流量 { while(now!=s) { flow[pre[now]]+=low; flow[pre[now]^1]-=low; now=po[pre[now]^1].v; } maxflow+=low;//更新最大流 low=inf; } break; } } if(i==-1) { if(--gap[lev[now]]==0) return maxflow;//间隙优化,当出现断层时,残流图中不存在增广流,即返回当前最大流 cur[now]=head[now];//当前顶点now已经走过需要重新退回该顶点头结点 lev[now]=inf; for(int k=head[now];k!=-1;k=po[k].next)//重新标号 { if(po[k].w>flow[k]&&lev[po[k].v]+1<lev[now]) lev[now]=lev[po[k].v]+1; } if(lev[now]<inf) gap[lev[now]]++; now=s; } }return maxflow; } int main() { int Cas=Read(); for(int k=1;k<=Cas;++k) { init(); int n=Read(); int m=Read(); for(int i=0;i!=m;++i) { int a=Read(); int b=Read(); int c=Read(); add(a,b,c); add(b,a,0); } printf("Case %d: %d\n",k,sap(1,n)); }return 0; }
方法二:E-k算法:
#include<iostream> #include<cstdio> #include<string.h> #include<string> #include<queue> #define N 16 using namespace std; int flow[N][N]; int cap[N][N]; int pre[N]; int a[N]; int max_flow(int sta,int end) { memset(flow,0,sizeof(flow)); int _maxflow=0; queue<int>Q; while(1) { memset(a,0,sizeof(a)); a[sta]=0xffffff; Q.push(sta); while(!Q.empty()) { int u=Q.front();Q.pop(); for(int i=1;i<=end;++i) if(!a[i]&&cap[u][i]>flow[u][i]) { a[i]=min(a[u],cap[u][i]-flow[u][i]); Q.push(i); pre[i]=u; } } if(!a[end]) return _maxflow; for(int v=end;v!=sta;v=pre[v]) { flow[pre[v]][v]+=a[end]; flow[v][pre[v]]-=a[end]; } _maxflow+=a[end]; } } int main() { int T; scanf("%d",&T); for(int k=1;k<=T;++k) { int n,m; scanf("%d%d",&n,&m); memset(cap,0,sizeof(cap)); for(int i=0;i!=m;++i) { int a,b,c; scanf("%d%d%d",&a,&b,&c); cap[a][b]+=c; } printf("Case %d: %d\n",k,max_flow(1,n)); }return 0; }