网络流问题:最大流及其算法

来源:互联网 发布:java内部类实例化 编辑:程序博客网 时间:2024/06/05 02:05

一、概念引入

首先要先清楚最大流的含义,就是说从源点到经过的所有路径的最终到达汇点的所有流量和。

      流网络G=(V,E)是一个有向图,其中每条边(u,v)∈E均有一个非负容量c(u,v)>=0。如果(u,v)不属于E,则假定c(u,v)=0。流网络中有两个特别的顶点:源点s和汇点t。下图展示了一个流网络的实例(其中斜线左边的数字表示实际边上的流,右边的数字表示边的最大容量):

image

二、基础知识准备

      对一个流网络G=(V,E),其容量函数为c,源点和汇点分别为s和t。G的流f满足下列三个性质: 
      容量限制:对所有的u,v∈V,要求f(u,v)<=c(u,v)。 
      反对称性:对所有的u,v∈V,要求f(u,v)=-f(v,u)。 
      流守恒性:对所有u∈V-{s,t},要求∑f(u,v)=0 (v∈V)。

容量限制说明了从一个顶点到另一个顶点的网络流不能超过设定的容量,就好像是一个管道只能传输一定容量的水,而不可能超过管道体积的限制;反对称性说明了从顶点u到顶点v的流是其反向流求负所得,就好像是当参考方向固定后,站在不同的方向看,速度一正一负;而流守恒性说明了从非源点或非汇点的顶点出发的点网络流之和为0,这有点类似于基尔霍夫电流定律,且不管什么是基尔霍夫电流定律,通俗的讲就是进入一个顶点的流量等于从该顶点出去的流量,如果这个等式不成立,必定会在该顶点出现聚集或是枯竭的情况,而这种情况是不应该出现流网络中的,所以一般的最大流问题就是在不违背上述原则的基础上求出从源点s到汇点t的最大的流量值,显然这个流量值应该定义为从源点出发的总流量或是最后聚集到t的总流量,即流f的值定义为|f|=∑f(s,v) (v∈V)。

在求解最大流的问题前,必须对三个概念有所了解:残留网络,增广路径和割。下面先给出这三个概念的基本内容。 
      a.在给定的流网络G=(V,E)中,设f为G中的一个流,并考察一对顶点u,v∈V,在不超过容量c(u,v)的条件下,从u到v之间可以压入的额外网络流量,就是(u,v)的残留容量,就好像某一个管道的水还没有超过管道的上限,那么就这条管道而言,就一定还可以注入更多的水。残留容量的定义为:cf(u,v)=c(u,v)-f(u,v)。而由所有属于G的边的残留容量所构成的带权有向图就是G的残留网络。下图就是上面的流网络所对应的残留网络:

对比于:

image


image

残留网络中的边既可以是E里面的边,也可以是此边的反向边。只有当两条边(u,v)和(v,u)中,至少有一条边出现在初始网络中时,边(u,v)才会出现在残留网络中。下面是一个有关残留网络的定理,若f是G中的一个流,Gf是由G导出的残留网络,f'是Gf中的一个流,则f+f'是G中一个流,且其值|f+f'|=|f|+|f'|。证明时只要证明f+f'这个流在G中满足之前所讲述的三个原则即可。在这里只给出理解性的证明,可以想象如果在一个管道中流动的水的总流量为f,而在该管道剩余的流量中存在一个流f'可以满足不会超过管道剩余流量的最大限,那么将f和f'合并后,也必定不会超过管道的总流量,而合并后的总流量值也一定是|f|+|f'|。

b.增广路径p为残留网络Gf中从s到t的一条简单路径。根据残留网络的定义,在不违反容量限制的条件下,G中所对应的增广路径上的每条边(u,v)可以容纳从u到v的某额外正网络流。而能够在这条路径上的网络流的最大值一定是p中边的残留容量的最小值。这还是比较好理解的,因为如果p上的流大于某条边上的残留容量,必定会在这条边上出现流聚集的情况。所以我们将最大量为p的残留网络定义为:cf(p)=min{cf(u,v) | (u,v)在p上}。而结合之前在残留网络中定理,由于p一定是残留网络中从s到t的一条路径,且|f'|=cf(p),所以若已知G中的流f,则有|f|+|cf(p)|>|f|且|f|+|cf(p)|不会超过容量限制。

c.流网络G(V,E)的割(S,T)将V划分成为S和T=V-S两部分,使得s∈S,t∈T。如果f是一个流,则穿过割(S,T)的净流被定义为f(S,T)=∑f(x,y) (x∈S,y∈T),割(S,T)的容量为c(S,T)。一个网络的最小割就是网络中所有割中具有最小容量的割。设f为G中的一个流,且(S,T)是G中的一个割,则通过割(S,T)的净流f(S,T)=|f|。因为f(S,T)=f(S,V)-f(S,S)=f(S,V)=f(s,V)+f(S-s,V)=f(s,V)=|f|(这里的公式根据f(X,Y)=∑f(x,y) (x∈X,y∈Y)的定义,以及前面的三个限制应该还是可以推出来的,这里就不细讲了)。有了上面这个定理,我们可以知道当把流不断增大时,流f的值|f|不断的接近最小割的容量直到相等,如果这时可以再增大流f,则f必定会超过某个最小的割得容量,则就会存在一个f(S,T)<=c(S,T)<|f|,显然根据上面的定理这是不可能。所以最大流必定不超过网络最小割的容量。

综合上面所讲,有一个很重要的定理:最大流最小割定理

如果f是具有源s和汇点t的流网络G=(V,E)中的一个流,则下列条件是等价的: 
      1) f是G中一个最大流。 
      2) 残留网络Gf不包含增广路径。 
      3) 对G的某个割(S,T),有|f|=c(S,T)。


*********************************** 分割线 ***********************************

上面是比较形式化的定义,比较适合学者之间讨论(撕逼)。下面开始通俗的说法:

最基础的最大流求法,即bfs找增广路法,也就是EK法,全名是Edmond-Karp,其实我倒是觉得记一下算法的全名和来历可以不时的拿出来装一装X。比如说这个,EK算法首先由俄罗斯科学家Dinic在1970年提出,没错,就是dinic算法的创始人,实际上他提出的也正是dinic算法,在EK的基础上加入了层次优化,这个我们以后再说,1972年Jack Edmonds和Richard Karp发表了没有层次优化的EK算法。但实际上他们是比1790年更早的时候就独立弄出来了。
你看,研究一下历史也是很有趣的。
    扯远了,首先来看一下基本的网络流最大流模型。
    有n个点,有m条有向边,有一个点很特殊,只出不进,叫做源点,通常规定为1号点。另一个点也很特殊,只进不出,叫做汇点,通常规定为n号点。每条有向边上有两个量,容量和流量,从i到j的容量通常用c[I,j]表示,流量则通常是f[I,j]。通常可以把这些边想象成道路,流量就是这条道路的车流量,容量就是道路可承受的最大的车流量。很显然的,流量<=容量。而对于每个不是源点和汇点的点来说,可以类比的想象成没有存储功能的货物的中转站,所有”进入”他们的流量和等于所有从他本身”出去”的流量。
    把源点比作工厂的话,问题就是求从工厂最大可以发出多少货物,是不至于超过道路的容量限制,也就是,最大流。
    比如这个图。每条边旁边的数字表示它的容量。
    
下面我们来考虑如何求最大流。

    首先,假如所有边上的流量都没有超过容量(不大于容量),那么就把这一组流量,或者说,这个流,称为一个可行流。一个最简单的例子就是,零流,即所有的流量都是0的流。

我们就从这个零流开始考虑,假如有这么一条路,这条路从源点开始一直一段一段的连到了汇点,并且,这条路上的每一段都满足流量<容量,注意,是严格的<,而不是<=。那么,我们一定能找到这条路上的每一段的(容量-流量)的值当中的最小值delta。我们把这条路上每一段的流量都加上这个delta,一定可以保证这个流依然是可行流,这是显然的。
    这样我们就得到了一个更大的流,他的流量是之前的流量+delta,而这条路就叫做增广路。

    我们不断地从起点开始寻找增广路,每次都对其进行增广,直到源点和汇点不连通,也就是找不到增广路为止。当找不到增广路的时候,当前的流量就是最大流,这个结论非常重要。
寻找增广路的时候我们可以简单的从源点开始做bfs,并不断修改这条路上的delta量,直到找到源点或者找不到增广路。
这里要先补充一点,在程序实现的时候,我们通常只是用一个c数组来记录容量,而不记录流量,当流量+1的时候,我们可以通过容量-1来实现,以方便程序的实现。
Bfs过程的半伪代码:下面另给一个C++版的模板:

int BFS(){    int i,j,k,v,u;    memset(pre,-1,sizeof(pre));    for(i=1;i<=n;++i)flow[i]=max_int;     queue<int>que;    pre[start]=0;    que.push(start);    while(!que.empty())    {        v=que.front();        que.pop();         for(i=1;i<=n;++i)        {            u=i;            if(u==start||pre[u]!=-1||map[v][u]==0)continue;            pre[u]=v;            flow[u]=MIN(flow[v],map[v][u]);            que.push(u);        }    }    if(flow[end]==max_int)return -1;    return flow[end];}

但事实上并没有这么简单,上面所说的增广路还不完整,比如说下面这个网络流模型。


我们第一次找到了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,等于没有流量。
这就是这个算法的精华部分,利用反向边,使程序有了一个后悔和改正的机会。而这个算法和我刚才给出的代码相比只多了一句话而已。其C++实现如下:

#define maxn 220#define INF 0x7f7f7f7fint cap[maxn][maxn],flow[maxn][maxn];int pre[maxn],res[maxn];//res[i] 残量int Edmonds_Karp(int start,int end){    int maxflow=0;    memset(flow,0,sizeof(flow));    memset(pre,0,sizeof(pre));    queue<int> q;    while(true)    {        memset(res,0,sizeof(res));        res[start]=INF;        q.push(start);        while(!q.empty()) //BFS寻找增广路        {            int u=q.front();            q.pop();            for(int v=1;v<=end;v++)                if(!res[v]&&cap[u][v]>flow[u][v])                {                    res[v]=min(res[u],cap[u][v]-flow[u][v]);//start-v路径上的最小残量                    //记录v的父亲,并加入队列中                    pre[v]=u;                    q.push(v);                }        }        if(res[end]==0) return maxflow;//无法继续更新最大流量,则当前流已经是最大流        for(int u=end;u!=start;u=pre[u])//从汇点往回走        {            flow[pre[u]][u]+=res[end];//更新正向流            flow[u][pre[u]]-=res[end];//更新反向流        }        maxflow+=res[end]; //更新从s流出的总流量    }}int main(){    //    memset(cap,0,sizeof(cap));    //    for(/**/)    {        int u,v,s;        scanf("%d %d %d",&u,&v,&s);        cap[u][v]+=s;//要考虑到重边问题    }    //    return 0;}
1 0
原创粉丝点击