最大流算法之-增广路径(path augmentation)and压入与重标记算法(push-relable)

      本人最近在看最小割/最大流的相关文章,和增广路径、压入与重标记算法相关。于是就对这两个算法进行了理解,在查看了他人的资料之后,整合之后生成了本篇文章。


增广路径(path augmentation)

        思想:从任意一个可行流(如零流)出发,找到一条源s到汇t的增广路,并在该增广路上增加流值,于是得到一个新的可行流。循环此过程直到找不出s到t的新的增广路。


        该算法关键是找到s到t的增广路,这可通过标号法实现,具体规则为:

·   一个节点先进行标号,再检查。一个节点可处于三个状态之一:已标号并且已检查;已标号但未检查;未标号。每次检查是从已标号的节点开始,所以设置源点s为永久标号点,当其他都未标号时,算法从已标号的源点开始。

·   一个节点i的标号表现形式由两个分量组成(+j, δ(i))或(-j, δ(i)) ,第一个分量+表示前继点,-表示后继点(即某条通过节点i的可行流的前一个节点j或后一个节点j),第二个分量表示了允许增加的流量 δ(i)=min(δ(j), cij-fij ) 。假设G(V,E) 是一个有限的有向图,它的每条(u,v)∈E都有一个非负值实数的容量c(u,v) ;f(u,v) 是由uv流。

·   从每个已标号但未检查的节点i开始,对其进行检查:检查该所相邻的所有节点j(前继点或后继点),如果存在未标号点j使得有向弧(ij)的现有流量 fij <该弧的容量 cij ,则对该节点j进行前继标记(+i, δ(j)) 其中 δ(j)=min( δ(i), cij-fij ) ;或者存在未标号点j使得有向弧(j,i)的现有流量 fji >0,则对该节点j进行后继标记(-i, δ(j)) 其中 δ(j)=min( δ(i), fji ) 。

 

算法步骤

1°. 令x=(xij)是任意整数可行流,给s一个永久标号 (*,∞);

2°. (1)如果所有标号顶点都已检查,转第4步 

     (2)找到一个已标号但未检查的顶点i,做如下检查:对每一条有向弧(i,j)且j未标号, 如果fij < cij 则给j标号(+i,  δ(j)),δ(j)=min(δ(i), cij-fij) ;对每一条有向弧(j,i)且j未标号,如果 fji >0,则给j标号(-i, δ(j)),δ(j)=min(δ(i), fji) 。

     (3)如果汇t被标记,转3°,否则转(1)

3°. 根据得到的增广路上各项标号来增加流量,并抹去除了源s外的标号转2°

4°. 此时当前流是最大流,且把所有标好点的集合记为S,(S, /S)是G的最小割。


算法的 伪代码

1  for each edge (u, v) ∈ E[G]
2       do f[u, v] ← 0    //f[u, v]为顶点u,v之间的网络流
3          f[v, u] ← 0
4  while there exists a path p from s to t in the residual network Gf    //Gf 为残留网络
5   do cf(p) ← min {cf(u,v) : (u, v) is in p}     //cf(p) 为增广路径p的残留容量,cf(u,v)为边(u,v)的残留容量
6   for each edge (u, v) in p
7   dof[u, v] ← f[u, v] + cf(p)
8   f[v, u] ← -f[u, v]


下图为实验例子的详细图解:(2个算法同为此例子)




增广路径的代码如下:

#include<iostream>
#include<math.h>
using namespace std;

#define INFI 1000
typedef struct _mark//MARK:(+j, δ(i))结构体。pre_suc:前一个连接点。max_incr:最大增量的流量即δ(i)
{
 int pre_suc;
 int max_incr;
}MARK;

int iteration = 0;//增广路径的次数
const int N = 100;
                                          
bool isMark[N], isCheck[N], isDone;//isMark标记。isChec检查
MARK markList[N];
int c[N][N], f[N][N];
int n; //顶点数
int min(int a,int b)
{
	return a>b?b:a;
}

void Mark(int index, int pre_suc, int max_incr)//index讲标记的点。pre_suc已标记,正在检查的点
{
 isMark[index] = true;

 markList[index].pre_suc = pre_suc;
 markList[index].max_incr = max_incr;
}
void Check(int i)
{
 isCheck[i] = true;

 for (int j=0;j<=n;j++)
 {
  if (c[i][j]>0 && !isMark[j] && c[i][j]>f[i][j])//对每一条有向弧(i,j)且j未标号, 如果fij < cij 则给j标号(+i,  δ(j)),δ(j)=min(δ(i), cij-fij) 
   Mark(j, i, min(markList[i].max_incr, c[i][j]-f[i][j]));
  if(c[j][i]>0 && !isMark[j] && f[j][i]>0)//对每一条有向弧(j,i)且j未标号,如果 fji >0,则给j标号(-i, δ(j)),δ(j)=min(δ(i), fji) 
   Mark(j, -i, min(markList[i].max_incr, f[j][i]));
 }
}

int Maxflow()//最大流计算
{
 int flow =0;
 for (int i=0;i<n;i++)
 {
	 if(isMark[i])
		 for(int j=1;j<n;j++)
		 {
			 if(j!=i&&!isMark[j]) flow += c[i][j];
		 }
  
 }
 return flow;
}

void Mincut()
{
 int i = 0;
 while (i<n)
 {
  if(isMark[i])
  cout<<i<<"  ";
  i++;
 }
}

int IncrFlowAuxi(int index)//辅助函数:计算增广路径中的最大可增量
{
 if(index==0) return markList[index].max_incr;

 int prev = markList[index].pre_suc;
 int maxIncr = markList[index].max_incr;
 return min(maxIncr, IncrFlowAuxi(prev));
}

void IncrFlow()//增广路径的增加
{
 iteration++;
 int incr = IncrFlowAuxi(n-1); //最大可增量
 int index = n-1;
 int prev;
 while(index!=0)
 {
  prev = markList[index].pre_suc;
  f[prev][index] += incr; //增广路径增加后,相应的流量进行更新
  f[index][prev]=-f[prev][index];// do f[u, v] ← f[u, v] + cf(p)  f[v, u] ← -f[u, v]
  index = prev;
 }
}

//ford_fulkerson算法
int ford_fulkerson()
{
 
 int i;
 while (1)
 {
  isDone = true;
  i=0;
  while(i<n)
  {
    if (isMark[i] && !isCheck[i])  //判断是否所有标记的点都已被检查:若是,结束整个算法
    {
     isDone = false;
     break;
    }
    i++;
  }

  if (isDone) //算法结束,则计算最小割和最大流
  {
   Mincut();
   return Maxflow();
   break;
  }
  
  while (i<n)
  {
   if(isMark[i] && !isCheck[i])
   {
    Check(i);
    i = 0;
   }
   if(isMark[n-1]) //如果汇t被标记,说明找到了一条增广路径,则增加该条路径的最大可增加量
   {
    IncrFlow();
    memset(isMark+1, false, n-1); //增加该增广路径后,除了源s,其余标记抹去
    memset(isCheck, false, n);
   }
   else i++;
  }
 }
}

int main()
{
 int m, i, j,k;
 cout<<"顶点个数:";
 cin>>n;
 cout<<"边数:";
 cin>>m;
 for ( k = 0; k < n; ++k)
 {
  memset(c[k], 0, sizeof(c[0][0])*n);
  memset(f[k], 0, sizeof(f[0][0])*n);  //初始各分支流量为0
  memset(isMark, false, n);
  memset(isCheck, false, n);
 }
 isMark[0] = true; //给源做永久标记
 markList[0].max_incr = INFI;
 markList[0].pre_suc = INFI;
 cout<<"各边的数值:";
 for(k=0;k<m;k++)
 {
  cin>>i;
  cin>>j;
  cin>>c[i][j];
 }
 cout<<"segment set S = {";
 int maxflow=ford_fulkerson();
 cout<<"}"<<endl;
 cout<<"max flow ="<<maxflow<<endl;
 return 0;
}


ps:S点为0点,其余点为1到n-1(此为本代码的缺陷,若想改可以自己改)

下面是运行的结果图:





压入和重标记算法(push-relable)

压入和重标记算法(push-relable)引入了一个新的概念叫做余流,余流的定义为e(u)=f(V,u)。
与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|,以便把所有顶点中的余流送回源点。当除去源点和汇点的所有水库的水均为空时,我们就得到了最大流了。

思想:先加入充足的流(跟s相连的所有边的容量之和),加入之后呢,再慢慢一个边一个边的向汇点渗透。直到没法再渗透(类似于ford-fulkerson算法中找不到增广路径了),那么这时再把一些剩余的流回收到source就可。
        主要分为两个步骤:push和relabel。push表示从任一个节点找出次节点的存水量和另一个相邻节点的剩余流量,比较哪一个小,借此可得此节点在增加该路径的流量之后是否还有存水量。要实现该push的操作必须满足三个条件:该点存水量>0,管线的容量大于流量,该点高度大于另一个点高度。relabel表示某一个节点存水量大于0但水流不出去时,增大该节点高度,使得该节点的存水量流入比它低的节点。


算法步骤
  1. 初始化前置流:将与源点s相连的管道流量f(0,i)设为该管道的容量,即 f(0,i)=c(0,i);将源点s的高度h(0)=V,(V表示图的顶点个数),其余顶点高度h(i)=0;将源的点余量e(0)设为源容量减去源的流出量,即e(0)=-∑f(0,i)=-∑c(0,i),与源s相连的点余量设为该点的流入量e(i)=c(0,i),其余点都为0。
  2. 构造一个存储顶点的队列vlist,用以检查点的压栈。从源点s出发,将与之相连的顶点压入栈(s不入栈,与s相连的点入栈)。
  3. 每次从栈中取首个元素,即某一个点,检查其点余量e(i),若不为0,表示要对该点进行操作——重标记或者压入流:检查与该点i全部的相邻点j,若该点比它相邻点的高度大h(i)>h(j)且该管道的容量c(i,j)大于流量f(i,j)时,将该点的余量以最大方式压入该管道delta=min(e(i), c(i,j)-f(i,j)), 点余量e、流量f相应的进行减加,另外在队列中加入满足点余量e(j)>0的相邻点j(vlist.push(j); j原不存在该队列中);若没有相邻点满足上述条件,则将该点的高度值h(i)根据相邻点j进行增加,h(i)=min(h(j))+1。以上的重标记或压入流操作循环进行,直至该点的余量e(i)为0。
  4. 重复第3步,直至队列vlist中没有元素,停止算法,最后输出汇点t的余量e(t), t=V-1, 该值就是最后所求的最大流。


方法的流程

1.给源点s一个足够(等于源点邻接边的容量之和)的流,流向它的邻近顶点,使它的邻近顶点构成一个活动顶点的集合A

2.遍历活动顶点集合A的每一个点v,对v进行如下操作:

(1)对v的每一条邻接边以边容量的流向前推进流(如果每条邻边都以边容量推进过的(达到最大推进),则执行(2)),并将该顶点加入集合A

(2)若(1)操作后顶点v的还有盈余量(e(v)>0),则将流量退回(逆推进)给上一个顶点(流给它的顶点),最后将v从集合A中移除,其中退回给的上一个顶点就变成了活动顶点要进入集合A

3.直到活动顶点集合A为空为止,流向汇点t的流量就是最大流量。



Push Relabel算法的流程

1)、构造初始流。将源点高度标记为n,其余点高度均为0;

2)、如果残留网络中不存在活结点,则算法结束。否则继续执行下一步;

3)、选取活结点,如果该结点出边为可推流边,则沿此边推流,否则将该节点高度增至可流向的高度值最低的点的高度值+1,执行步骤2)。


压入与重标记算法实现代码

#include <iostream>
#include <cstring>
using namespace std;
const int MAX_SIZE = 100;
const int INF = 1 << 30;
int capacityGraph[MAX_SIZE][MAX_SIZE];//即c[u][v]
int flowMap[MAX_SIZE][MAX_SIZE];//即f[u][v]
int height[MAX_SIZE];//高度h()
int excess[MAX_SIZE];//余流e()
int src, des;
int vertex_num, edge_num,account=0;//vertex_num顶点数,edge_num边数

void init()//初始设置
{
    memset( capacityGraph, 0, sizeof( capacityGraph ) );
    memset( flowMap, 0, sizeof( flowMap ) );
    memset( height, 0, sizeof( height ) );
    memset( excess, 0, sizeof( excess ) );
    cout<<"输入顶点数和边数 : ";
    cin>>vertex_num>>edge_num;
	cout<<"各边的数值:";
    for( int i = 1; i <= edge_num; ++i ){
        int start, end, cap;
        cin>>start>>end>>cap;
        capacityGraph[start][end] = cap;
    }
    src = 1;
    des = vertex_num;
    height[src] = vertex_num;
}

void preFlow()//前置流
{
    for( int i = src; i <= des; ++i ){
        if( capacityGraph[src][i] > 0 ){
            const int flow = capacityGraph[src][i];
            flowMap[src][i] += flow;
            flowMap[i][src] = - flowMap[src][i];
            excess[src] -= flow;
            excess[i] += flow;
        }
    }
}

void push( int start, int end )//压入
{
    int flow =  excess[start]>(capacityGraph[start][end] - flowMap[start][end] )?(capacityGraph[start][end] - flowMap[start][end] ):excess[start];
    flowMap[start][end] += flow;
    flowMap[end][start] = -flowMap[start][end];
    excess[start] -= flow;
    excess[end] += flow;
}

bool reLabel( int index )//重标记
{
    int minestHeight = INF;

    for( int i = src; i <= des; ++i ){
        if( capacityGraph[index][i] - flowMap[index][i] > 0 ){
            minestHeight = minestHeight> height[i]?height[i]:minestHeight;
        }
    }
    if( minestHeight == INF ) return false;
    height[index] = minestHeight + 1;
	
    for(  i = src; i <= des; ++i ){
        if( excess[index] == 0 ) break;
        if( height[i] == minestHeight && capacityGraph[index][i] > flowMap[index][i] ){
            push( index, i );
        }
    }
    return true;
}

void pushReLabel()
{
    bool flag = true;
    preFlow();
    while( true ){

        if( flag == false ) break;
        flag = false;
        for( int i = src; i <= des - 1; ++i ){
            if( excess[i] > 0 ) flag = flag || reLabel( i );//此处每轮循环只执行一次函数reLabel( i ),当flag为TRUE时,不执行函数reLabel( i )
        }
    }
}

int main(){
    init();
    pushReLabel();
    cout<<"max flow : "<<excess[des]<<endl;
    return 0;
}

上述例子的运行结果图:



下面是本文参考的资料:

如果本文有什么错误的地方,请告知!





  • 8
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在 Ford-Fulkerson 算法中,我们需要通过不断地寻找增广路径来不断地增加流量,直到无法找到增广路径为止。因此,一个合适的增广路径长度的上界是非常有用的。 首先,我们可以注意到,对于一个二分图 G,它的最大流最多只能达到 |L|×|U|。这是因为,对于任意一条从 L 到 U 的路径,最多只能流过一单位的流量。因此,在每次增广时,我们最多只能增加 1 的流量。因此,如果我们设初始流量为 0,那么当流量达到 |L|×|U| 时,算法一定会终止。 接下来考虑如何给出一个更紧确的上界。我们可以使用最短增广路径算法,即每次寻找增广路径时,我们选择当前权最小的路径。在这种情况下,我们可以证明,最短增广路径的长度上界为 2|V| - 1。 证明如下: 假设我们有一个最短增广路径 P,其长度为 k。因为这是一个增广路径,所以我们可以将其看做一个交替路径,即 P = (s, v1, u1, v2, u2, ..., vk-1, uk-1, vk)。其中 s 是源点,v1,v2,...,vk-1 是 L 中的结点,u1,u2,...,uk-1 是 U 中的结点,而 vk 是汇点。 我们可以将这个交替路径分成两个部分: - 从源点 s 开始,走到 v1 的路径 P1。 - 从 vk-1 开始,走到汇点 t 的路径 P2。 由于这是一个最短增广路径,所以对于任意一条增广路径 P',其长度一定不小于 k。因此,我们可以将 P' 分成两个部分 P'1 和 P'2,使得 P'1 和 P1 相连,P'2 和 P2 相连。这是因为如果 P'1 和 P1 不相连,那么从 s 到 P'1 再到 P1 的路径长度就小于 k,不符合最短增广路径的定义。 同样地,如果 P'2 和 P2 不相连,那么从 P'2 到 t 再到 vk 的路径长度也小于 k,也不符合最短增广路径的定义。因此,我们可以将 P' 分成这样的两个部分。 由于 P1 和 P2 中的结点都不在同一侧,因此它们之间至少有一个交点。假设交点是结点 x,它在 P1 中的位置是 i,而在 P2 中的位置是 j。因为 P 是一个交替路径,所以 i+j=k-1。 现在我们来考虑结点 x 的度数。因为 x 在 P1 和 P2 中都出现了,所以它的度数至少为 2。如果它的度数大于等于 3,那么我们可以找到另外一条增广路径 P',其中 P'1 和 P1 相同,但是 P'2 不同于 P2。我们可以通过将 P2 中的结点 x 换成它相邻的另外一个结点 y,来构造这样的 P'2。因为 x 的度数大于等于 3,所以一定存在这样的结点 y。因此,P' 的长度不小于 k,这与 P 是最短增广路径的定义矛盾。 因此,我们得出结论:结点 x 的度数不能大于等于 3。因为 x 在 L 中,而 L 中的结点之间没有边相连,所以 x 的度数只能为 2。 现在我们来考虑最坏情况。假设我们有一个包含了所有结点的二分图,即 |L|=|U|=|V|/2。在这种情况下,对于每个结点 x∈L,它的度数都是 2。因此,我们可以将交替路径 P 分成 |L| 条路径,每条路径都包含一个 L 中的结点和一个 U 中的结点。因为一共有 |V| 个结点,所以最多有 |V|/2 条这样的路径。因此,最短增广路径的长度上界为 2|V|/2=|V|。 综上所述,我们得出最短增广路径的长度上界为 2|V| - 1。在 Ford-Fulkerson 算法中使用这个上界,可以加快算法的收敛速度。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值