EK算法(网络流,最大流)

一、网络与网络流

给一个有向图(V,E),在V中指定一点,称为源点(记为vs),和另一点,称为汇点(记为vt),其余的点叫做中间点。对于E中每条弧(vi,vj)都对应一个正整数c(vi,vj)>=0(或简写为cij),称为f的容量,则赋权有向图N=(V,E,c,vs,vt)称为一个网络。所谓网络上的流,是指定义在弧集E上的一个函数f=f{f{vi,vj}},并称f(vi,vj)为弧(vi,vj)上的流量(下面简记为fij)。因此弧上有两个数,第一个表示容量cij,第二个表示流量fij。

二、可行流与最大流

在运输网络的实际问题中,我们可以看出,对于流有两个显然的要求:一是每个弧上的流量不能超过该弧的容量;二是中间点的流量为0,源点的净流出量和汇点的净流入量必相等且为这个方案的总输送量。因此有:

(1)容量约束:0≤fij≤cij,(vi,vj)∈E,
(2)守恒条件
对于中间点:流入量=流出量;对于源点与汇点:源点的净流出量vs(f)=汇点的净流入量(-vt(f))的流f,称为网络N上的可行流,并将源点s的净流量称为流f的流值v(f)
网络N中流值最大的流f*称为N的最大流。 

三、可增广路径

所为可增广路径,是指这条路径上的流可以修改,通过修改,使得整个网络的流值增大。

设f是一个可行流,P是从源点s到汇点t的一条路径,若P满足下列条件:

(1)在p上的所有前向弧(vi→vj)都是非饱和弧,即0≤fij<cij
(2)在p上的所有后向弧(vi←vj)都是非零弧,即0<fij≤cij
则称p为(关于可行流f的)一条可增广路径

四、最大流定理

当且仅当不存在关于f*的增广路径,可行流f*为最大流。

五、最大流算法

算法思想:最大流问题实际上是求一可行流{fij},使得v(f达到最大。若给了一个可行流f,只要判断N中有无关于f的增广路径,如果有增广路径,改进f, 得到一个流量增大的新的可行流;如果没有增广路径,则得到最大流。

1.寻求最大流的标号法(Ford,Fulkerson)
 从一个可行流(一般取零流)开始,不断进行以下的标号过程与调整过程,直到找不到关于f的可增广路径为止。
    (1)标号过程
    在这个过程中,网络中的点分为已标号点和未标号点,已标号点又分为已检查和未检查两种。每个标号点的标号信息表示两个部分:第一标号表明它的标号从哪一点得到的,以便从vt开始反向追踪找出也增广路径;第二标号是为了表示该顶点是否已检查过。
    标号开始时,给vs标上(s,0),这时vs是标号但末检查的点,其余都是未标号的点,记为(0,0)。
    取一个标号而未检查的点vi,对于一切未标号的点vj
    A.对于弧(vi,vj),若fij<cij,则给vj标号(vi,0),这时,vj点成为标号而未检查的点。
    B.对于弧(vi,vj),若fji>0,则给vj标号(-vi,0),这时,vj点成为标号而未检查的点。
    于是vi成为标号且已检查的点,将它的第二个标号记为1。重复上述步骤,一旦vt被标上号,表明得到一条从vi到vt的增广路径p,转入调整过程
    若所有标号都已检查过去,而标号过程进行不下去时,则算法结束,这时的可行流就是最大流。

  (2)调整过程
    从vt点开始,通过每个点的第一个标号,反向追踪,可找出增广路径P。例如设vt的第一标号为vk(或-vk),则弧(vk,vt)(或 相应地(vt,vk))是p上弧。接下来检查vk的第一标号,若为vi(或-vi),则找到(vi,vk)(或相应地(vk,vi))。再检查vi的第一 标号,依此类推,直到vs为止。这时整个增广路径就找到了。在上述找增广路径的同时计算Q:
          Q=min{min(cij-fij),minf*ij}
    对流f进行如下的修改:
    f'ij =  fij+Q   (vi,vj)∈ P的前向弧的集合
    f'ij =  fij-Q   (vi,vj)∈ P的后向弧的集合
    f'ij =  f*ij     (vi,vj)不属于P的集合
接着,清除所有标号,对新的可行流f’,重新进入标号过程。

-------------------------------------------------------------------------------------------------------------------------------------------------------------------

下面说一下EK算法

1、首先,什么是最大流

最大流就是从源点到经过的所有路径的最终到达汇点的所有流量和

2、EK算法的核心是反复寻找源点S到汇点t之间的增广路径,若有,找出增广路径上每一段[容量-流量]的最小值delta,若无,则结束。在寻找增广路径时,可以用BFS来找,并且更新残留网络的值(涉及到反向边)。

而找到delta后,则使最大流加上delta,更新为当前的最大流值。

这么一个图,求源点1,到汇点4的最大流

*** capacity存边的流量 ,进行ek求解

对于BFS找增广路:

1、flow[1]=INF,pre[1]=0;

源点1进队列,开始找增广路:

capacity[1][2]=40>0,则flow[2]=min(flow[1],40)=40;

capacity[1][4]=20>0,则flow[4]=min(flow[1],20)=20;

capacity[2][3]=30,则flow[3]=min(flow[2]=40,30)=30;

capacity[2][4]=20,但是pre[4]=1(已经在capacity[1][4]这遍历过4号点了)

capacity[3][4]......

当index=4(汇点),结束增广路的寻找

传递回increasement(该路径的流),利用pre前驱寻找路径

图也被改成

接下来同理

这就是最终完成的图,最终sumflow=20+20+10=50(这个就是最大流的值)

PS,为什么要有反向边呢? 

我们第一次找到了1-2-3-4这条增广路,这条路上的delta值显然是1。于是我们修改后得到了下面这个流。(图中的数字是容量)

这时候(1,2)和(3,4)边上的流量都等于容量了,我们再也找不到其他的增广路了,当前的流量是1。

但这个答案明显不是最大流,因为我们可以同时走1-2-4和1-3-4,这样可以得到流量为2的流。

那么我们刚刚的算法问题在哪里呢?问题就在于我们没有给程序一个”后悔”的机会,应该有一个不走(2-3-4)而改走(2-4)的机制。那么如何解决这个问题呢?回溯搜索吗?那么我们的效率就上升到指数级了。

而这个算法神奇的利用了一个叫做反向边的概念来解决这个问题。即每条边(I,j)都有一条反向边(j,i),反向边也同样有它的容量。

我们直接来看它是如何解决的:

在第一次找到增广路之后,在把路上每一段的容量减少delta的同时,也把每一段上的反方向的容量增加delta。即在Dec(c[x,y],delta)的同时,inc(c[y,x],delta)

我们来看刚才的例子,在找到1-2-3-4这条增广路之后,把容量修改成如下

这时再找增广路的时候,就会找到1-3-2-4这条可增广量,即delta值为1的可增广路。将这条路增广之后,得到了最大流2。

那么,这么做为什么会是对的呢?我来通俗的解释一下吧。

事实上,当我们第二次的增广路走3-2这条反向边的时候,就相当于把2-3这条正向边已经是用了的流量给”退”了回去,不走2-3这条路,而改走从2点出发的其他的路也就是2-4。(有人问如果这里没有2-4怎么办,这时假如没有2-4这条路的话,最终这条增广路也不会存在,因为他根本不能走到汇点)同时本来在3-4上的流量由1-3-4这条路来”接管”。而最终2-3这条路正向流量1,反向流量1,等于没有流量。

这就是这个算法的精华部分,利用反向边,使程序有了一个后悔和改正的机会。而这个算法和我刚才给出的代码相比只多了一句话而已。

至此,最大流Edmond-Karp算法介绍完毕。

 六、EK算法模板

#include<bits/stdc++.h>
using namespace std;
#define arraysize 201
int maxData=0x7fffffff;
int capacity[arraysize][arraysize];//记录残留网络的容量
int flow[arraysize];//标记从源点到当前节点实际还剩多少流量可用
int pre[arraysize];//标记在这条路径上当前节点的前驱,同时标记该节点是否在队列中
int n,m;
queue<int>myqueue;
int BFS(int src,int des)
{
    int i,j;
    while(!myqueue.empty())//队列清空
        myqueue.pop();
    for(i=1;i<m+1;++i)
    {
        pre[i]=-1;
    }
    pre[src]=0;
    flow[src]=maxData;
    myqueue.push(src);
    while(!myqueue.empty())
    {
        int index=myqueue.front();
        myqueue.pop();
        if(index==des)//找到了增广路径
            break;
        for(i=1;i<m+1;++i)
        {
            if(i!=src&&capacity[index][i]>0&&pre[i]==-1)
            {
                pre[i]=index;//记录前驱
                flow[i]=min(capacity[index][i],flow[index]);//关键:迭代的找增量
                myqueue.push(i);
            }
        }
    }
    if(pre[des]==-1)//残留途中不再存在增广路径
        return -1;
    else
        return flow[des];
}
int maxFlow(int src,int des)
{
    int increasement= 0;
    int sumflow = 0;
    while((increasement=BFS(src,des))!=-1)
    {
         int k = des;          //利用前驱寻找路径
         while(k!=src)
         {
              int last = pre[k];
              capacity[last][k] -= increasement; //改变正向边的容量
              capacity[k][last] += increasement; //改变反向边的容量
              k = last;
         }
         sumflow += increasement;
    }
    return sumflow;
}
int main()
{
    int i,j;
    int start,end,ci;
    while(cin>>n>>m)//边的个数和终点
    {
        memset(capacity,0,sizeof(capacity));
        memset(flow,0,sizeof(flow));
        for(i=0;i<n;++i)
        {
            cin>>start>>end>>ci;
            if(start == end)               //考虑起点终点相同的情况
               continue;
            capacity[start][end] +=ci;     //此处注意可能出现多条同一起点终点的情况
        }
        cout<<maxFlow(1,m)<<endl;
    }
    return 0;
}

最大流 EK算法

猜你喜欢

转载自blog.csdn.net/Destinymiao/article/details/81388542