您的位置:首页 > 其它

最大流算法 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优化后可成倍提升效率。

#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;  
}


具体分析见代码注释:有疑惑或没写清楚的地方欢迎指出~
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: