准备好基础知识,我们就要开始学习高大上的最大流啦~ :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:再说一遍不是加深理解么~)。怎样理解呢?由于接下来的模拟标号过程图有点多,所以另开一篇讲解(已经理解标号过程的就不需要看了):猛戳这里→_→http://blog.csdn.net/ycdfhhc/article/details/42585695
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;
}
具体分析见代码注释:有疑惑或没写清楚的地方欢迎指出~