与Ford-Fulkerson方法不同,压入和重标记算法不是检查整个残留网络来找出增广路径,而是每次仅对一个顶点进行操作,并且仅检查残留网络中该顶点的相邻顶点。压入和重标记算法引入了一个新的概念叫做余流,余流的定义为e(u)=f(V,u)。我们知道,在流网络满足三个限制条件的情况下有e(u)=0,但是在该算法的执行过程中,并不能保证流守恒,但是却保持了一个“前置流”,前置流满足反对称性、容量限制、和放宽条件的流守恒特性,而这个放宽条件的流守恒特性就是指e(u)>=0,当e(u)>0时,则称顶点u溢出。下面对压入和重标记算法给出一个更直观的理解。
继续把流网络中的边看成是运输的管道,与之前Ford-Fulkerson思想有所不同的是,这里我们将每个顶点看成是一个水库,此时,上面所讲的余流实际上就是某一个水库中的蓄水量。为了算出最终的最大流,我们是不允许水库中有余流的,所以就要将存在余流的水库中的水向其他能聚集液体的任意大水库排放,这个操作就是压入操作。而压入的方式则取决于顶点的高度,顶点的高度是一个比较抽象的概念,我们可以认为当余流从水库u压入其他水库及以后的过程中,水库u的高度随之增加,我们仅仅把流往下压,即从较高顶点压入向较低顶点压,这样就不会出现刚把一个流从u压入v后马上又被往回压的死循环的情况了。源点的高度固定为|V|,汇点的高度固定为0,所有其他顶点的高度开始时都是0,并逐步增加。算法首先从源点输送尽可能多的流出去,也就是恰好填满从源点出发每条管道,当流进入一个中间顶点时,它就聚集在该顶点的水库中,并且最终将从这里继续向下压入。
在压入的过程中可能会发生下列情况,离开顶点u且未被填满的管道所连接的顶点的高度与u相等或者高于u,根据我们的压入规则,这时候是没有办法继续往下压入的。为了使溢出顶点u摆脱其余流,就必须增加它的高度,即称为重标记操作。我们把u的高度增加到比其最低的相邻顶点(未被填满的管道所连接的顶点)的高度高一个单位。显然,当一个顶点被重标记后,至少存在着一条管道可以排除更多的流。
最终,有可能到达汇点的所有流均到达汇点,为了使前置流称为合法的流,根据算法的执行过程,算法会继续讲顶点的高度标记高于源点的固定高度|V|,以便把所有顶点中的余流送回源点。当除去源点和汇点的所有水库的水均为空时,我们就得到了最大流了。实现压入和重标记算法的具体代码如下:
struct Node{
int v;
Node *next;
}
Node g[MAX]; //用邻接表存储
int resi[MAX][MAX]; //残留容量
int e[MAX],h[MAX]; //顶点的余流和高度
int Push_Relabel(int s,int t,int n){
queue<int> que;
int i,u,_min,sum=0;
Node *p;
//初始化顶点高度和余流
memset(e,0,sizeof(e));
memset(h,0,sizeof(h));
h[s]=n;
e[0]=(1<<30);
que.push(s); //将源点进队
while(!que.empty()){
u=que.front();
que.pop();
for(p=g[u].next;p;p=p->next){
_min=resi[u][p->v]<e[u]?resi[u][p->v]:e[u]; //取顶点余流和相邻边的残留容量的最小值
//如果h[u]<=h[p->v],则应执行中标记操作;如果h[u]==h[p->v]+1,则执行压入操作
//但是我不清楚为什么h[u]>h[p->v]+1则边(u,p->v)一定不是残留图中的边?
if(_min&&(h[u]==h[p->v]+1 || u==s)){
resi[u][p->v]-=_min;
resi[p->v][u]+=_min;
e[u]-=_min;
e[p->v]+=_min;
if(p->v==t)sum+=_min; //如果到达了汇点,就将流值加入到最大流中
else if(p->v!=s)que.push(p->v); //只有既不是源点也不是汇点才进队
}
}
//如果不是源点且仍有余流,则重标记高度再进队。
//这里只是简单的将高度增加了一个单位,也可以向上面所说的一样赋值为最低的相邻顶点的高度高一个单位
if(u!=s&&e[u]){
h[u]++;
que.push(u);
}
}
return sum;
}