学习一下网络流的知识
感谢参考:
http://www.wutianqi.com/?p=3107
http://blog.csdn.net/y990041769/article/details/21026445
http://www.cnblogs.com/zsboy/archive/2013/01/27/2878810.html
http://kmplayer.iteye.com/blog/602734
http://chhaj5236.blog.163.com/blog/static/11288108120099725027512/
1.流网络G=(V,E)是一个有向图,其中每条边(u,v)∈E均有一个非负容量c(u,v)>=0。如果(u,v)不属于E,则假定c(u,v)=0。流网络中有两个特别的顶点:源点s和汇点t。下图展示了一个流网络的实例(其中斜线左边的数字表示实际边上的流,右边的数字表示边的最大容量):
对一个流网络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)。
容量限制说明了从一个顶点到另一个顶点的网络流不能超过设定的容量,一般的最大流问题就是在不违背上述原则的基础上求出从源点s到汇点t的最大的流量值,显然这个流量值应该定义为从源点出发的总流量或是最后聚集到t的总流量,即流f的值定义为|f|=∑f(s,v) (v∈V)。
2.在求解最大流的问题前,必须对三个概念有所了解:残留网络,增广路径和割。下面先给出这三个概念的基本内容。
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的残留网络。下图就是上面的流网络所对应的残留网络:
残留网络中的边既可以是E中边,也可以是它们的反向边。只有当两条边(u,v)和(v,u)中,至少有一条边出现在初始网络中时,边(u,v)才会出现在残留网络中。下面是一个有关残留网络的定理,若f是G中的一个流,Gf是由G导出的残留网络,f'是Gf中的一个流,则f+f'是G中一个流,且其值|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的值|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)。
3. 求解网络最大流算法有两种基本的方式:Ford-Fulkerson方法和压入与重标记算法
a.之所以称作Ford-Fulkerson方法,是因为此方法的核心思想就是,在构造的剩余网络中,寻找由S到T的增广路径,并且沿着增广路径更新剩余网络,知道剩余网络中不存在增广路径为止。虽然,核心思想是一样的,但是在寻找增广路径中存在DFS,BFS等方法,因此有不同的算法实现。最常用的是使用BFS的Edmonds-Karp算法。
/** * Edmond Karp * Max Flow * by Tanky Woo @ www.wutianqi.com */ #include <iostream> #include <queue> #include <algorithm> using namespace std; const int msize = 205; int N, M; // N--路径数, M--结点数 int r[msize][msize]; //残余网络邻接阵 int pre[msize]; // 记录结点i的前向结点为pre[i] bool vis[msize]; // 记录结点i是否已访问 // 用BFS来判断从结点s到t的路径上是否还有delta // 即判断s,t之间是否还有增广路径,若有,返回1 bool BFS(int s, int t) { queue<int> que; memset(pre, -1, sizeof(pre)); memset(vis, false, sizeof(vis)); pre[s] = s; vis[s] = true; que.push(s); int p; while(!que.empty()) { p = que.front(); que.pop(); for(int i=1; i<=M; ++i) { if(r[p][i]>0 && !vis[i]) { pre[i] = p; vis[i] = true; if(i == t) // 存在增广路径 return true; que.push(i); } } } return false; } int EK(int s, int t) { int maxflow = 0, d; while(BFS(s, t)) { d= INT_MAX; // 若有增广路径,则找出最小的delta for(int i=t; i!=s; i=pre[i]) d = min(d, r[pre[i]][i]);//两者求最小值 // 这里是反向边,看讲解 for(int i=t; i!=s; i=pre[i]) { r[pre[i]][i] -= d; r[i][pre[i]] += d; } maxflow += d; } return maxflow; } int main() { while(cin >> N >> M) { memset(r, 0, sizeof(r)); int s, e, c; for(int i=0; i<N; ++i) { cin >> s >> e >> c; r[s][e] += c; // 有重边时则加上c } cout << EK(1, M) << endl; } return 0; }
b. 与Ford-Fulkerson方法不同,压入和重标记算法不是检查整个残留网络来找出增广路径,而是每次仅对一个顶点进行操作,并且仅检查残留网络中该顶点的相邻顶点。压入和重标记算法引入了一个新的概念叫做余流。我们知道,在流网络满足三个限制条件的情况下有e(u)=0,放宽条件的流守恒特性就是指e(u)>=0,当e(u)>0时,则称顶点u溢出。下面对压入和重标记算法给出一个更直观的理解。
继续把流网络中的边看成是运输的管道,与之前Ford-Fulkerson思想有所不同的是,这里我们将每个顶点看成是一个水库,此时,上面所讲的余流实际上就是某一个水库中的蓄水量。为了算出最终的最大流,我们是不允许水库中有余流的,所以就要将存在余流的水库中的水向其他能聚集液体的任意大水库排放,这个操作就是压入操作。
而压入的方式则取决于顶点的高度,顶点的高度是一个比较抽象的概念,我们可以认为当余流从水库u压入其他水库及以后的过程中,水库u的高度随之增加,我们仅仅把流往下压,即从较高顶点压入向较低顶点压,这样就不会出现刚把一个流从u压入v后马上又被往回压的死循环的情况了。源点的高度固定为|V|,汇点的高度固定为0,所有其他顶点的高度开始时都是0,并逐步增加。算法首先从源点输送尽可能多的流出去,也就是恰好填满从源点出发每条管道,当流进入一个中间顶点时,它就聚集在该顶点的水库中,并且最终将从这里继续向下压入。
在压入的过程中可能会发生下列情况,所连接的顶点的高度与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; }