http://chuanwang66.iteye.com/blog/1446977
http://blog.csdn.net/smartxxyx/article/details/9293665
流网络的流量守恒的原则,根据这个原则我们可以知道,对网络的任意割,其净流量的都是相等的。
图的匹配问题与最大流问题(三)——最大流问题Ford-Fulkerson方法Java实现
SAP算法:
2. 网络流:
(1)容量限制:f(u,v)≤c(u,v) ==> 单向 流速受限
(2)反对称性:f(u,v)=-f(v,u) ==> 在管道中向不同方向看,水流一面迎面而来 ,一面向前推我
(3)流守恒性:f(V-s-t,V)=0 ==> A. 中间顶点不存储流 (流进=流出 )
B. 进入中间顶点的正网络流=离开该点的正网络流
注意:
流守恒性也可以写作f(V,V-s-t)=0,即流入一个顶点的总流为0;
在f(V-s-t,V)或f(V,V-s-t)的中,其中每一元素可以为正可以为负,不要以为不能为负(这是“残留网络”中的限制,网络流不受限)
引理26.1: G=<V,E>是流网络,f是G的一个流。
定义f(X,Y)=∑f(x,y),x∈X, y∈Y.
(1)任意X⊆V, 则 f(X,X)=0 ==>反对称性: f(u,v)= - f(v,u)
(2)任意X,Y⊆V, 则f(X,Y)=-f(Y,X) ==>(1)的推广
(3)X,Y,Z⊆ V, X∩Y=∅ 则f(X∪Y,Z)=f(X,Z)+f(Y,Z)
最大流最小割定理 :
(1)f是G的一个最大流 ==>达到流值|f|=f(s,V)最大
(2)残留网络Gf不包含增广路径 ==>不能再压入正网络流
(3)对G的某个割(S,T) ,存在|f|=c(S,T) ==>对最大流的限制来自最小容量的那个割
http://blog.csdn.net/smartxxyx/article/details/9293665
一、残留网络
顾名思义,残留网络是指给定网络和一个流,其对应还可以容纳的流组成的网络。具体说来,就是假定一个网络G=(V,E),其源点s,汇点t。设f为G中的一个流,对应顶点u到顶点v的流。在不超过C(u,v)的条件下(C代表边容量),从u到v之间可以压入的额外网络流量,就是边(u,v)的残余容量(residual capacity),定义如下:
r(u,v)=c(u,v)-f(u,v)
举个例子,假设(u,v)当前流量为3/4,那么就是说c(u,v)=4,f(u,v)=3,那么r(u,v)=1。
我们知道,在网络流中还有这么一条规律。从u到v已经有了3个单位流量,那么从反方向上看,也就是从v到u就有了3个单位的残留网络,这时r(v,u)=3。可以这样理解,从u到v有3个单位流量,那么从v到u就有了将这3个单位流量的压回去的能力。
我们来具体看一个例子,如下图所示一个流网络
其对应的残留网络为:
二、增广路径
在了解了残留网络后,我们来介绍增广路径。已知一个流网络G和流f,增广路径p是其残留网络Gf中从s到t的一条简单路径。形象的理解为从s到t存在一条不违反边容量的路径,向这条路径压入流量,可以增加整个网络的流值。上面的残留网络中,存在这样一条增广路径:
其可以压入4个单位的流量,压入后,我们得到一个新的流网络,其流量比原来的流网络要多4。这时我们继续在新的流网络上用同样的方法寻找增广路径,直到找不到为止。这时我们就得到了一个最大的网络流。
三、流网络的割
上面仅仅是介绍了方法,可是怎么证明当无法再寻找到增广路径时,就证明当前网络是最大流网络呢?这就需要用到最大流最小割定理。
首先介绍下,割的概念。流网络G(V,E)的割(S,T)将V划分为S和T=V-S两部分,使得s属于S,t属于T。割(S,T)的容量是指从集合S到集合T的所有边(有方向)的容量之和(不算反方向的,必须是S-àT)。如果f是一个流,则穿过割(S,T)的净流量被定义为f(S,T)(包括反向的,SàT的为正值,T—>S的负值)。将上面举的例子继续拿来,随便画一个割,如下图所示:
割的容量就是c(u,w)+c(v,x)=26
当前流网络的穿过割的净流量为f(u,w)+f(v,x)-f(w,v)=12+11-4=19
显然,我们有对任意一个割,穿过该割的净流量上界就是该割的容量,即不可能超过割的容量。所以网络的最大流必然无法超过网络的最小割。
流网络的流量守恒的原则,根据这个原则我们可以知道,对网络的任意割,其净流量的都是相等的。
和上面的割相比,集合S中少了u和v,从源点s到集合T的净流量都流向了u和v,而在上一个割图中,集合S到集合T的流量是等于u和v到集合T的净流量的。其中w也有流流向了u和v,而这部分流无法流向源点s,因为没有路径,所以最后这部分流量加上s到u和v的流量,在u和v之间无论如何互相传递流,最终都要流向集合T,所以这个流量值是等于s流向u和v的值的。将s比喻成一个水龙头,u和v流向别处的水流,都是来自s的,其自身不可能创造水流。所以任意割的净流量都是相等的。
万事俱备,现在来证明当残留网络Gf中不包含增广路径时,f是G的最大流。
假设Gf中不包含增广路径,即Gf不包含从s到v的路径,定义S={v:Gf中从s到v存在一条通路},也就是Gf中s能够有通路到达的点的集合,显然这个集合不包括t,因为s到t没有通路。这时,我们令T=V-S。那么(S,T)就是一个割。如下图所示:
那么,对于顶点u属于S,v属于T,有f(u,v)=c(u,v)。否则(u,v)就存在残余流量,因而s到u加上u到v就构成了一条s到v的通路,所以v就必须属于S,矛盾。因此这时就表明当前流f是等于当前的割的容量的,因此f就是最大流。
图的匹配问题与最大流问题(三)——最大流问题Ford-Fulkerson方法Java实现
FORD-FULKERSON(G,t,s)
1 for each edge(u,v)属于E(G)
2 do f[u,v]=0
3 f[v,u]=0
4 while there exists a path p from s to t in the residual network Gf
5 do cf(p)=min{cf(u,v):(u,v)is in p}
6 for each edge (u,v) in p
7 do f[u,v]=f[u,v]+cf(p)
8 f[v,u]=-f[u,v]
如果在4行中用广度优先搜索来实现对增广路径p的计算,即找到s到t的最短增广路径,能够改进FORD-FULERSON的界,这就是Ford-Fulkerson方法的Edmonds-Karp算法
证明该算法的运行时间为O(VE*E),易知,对流增加的全部次数上界为O(VE),每次迭代时间O(E)
- public class FordFulkerson
- {
- private int residualNetwork[][]=null;
- private int flowNetwork[][]=null;
- public final int N;
- int parent[];
- public FordFulkerson(int N)
- {
- this.N=N;
- parent=new int[N];
- }
- /**
- * 实现FordFulkerson方法的一种算法——edmondsKarp算法
- * @param graph
- * @param s
- * @param t
- * @return
- */
- public int edmondsKarpMaxFlow(int graph[][],int s,int t)
- {
- int length=graph.length;
- int f[][]=new int[length][length];
- for(int i=0;i<length;i++)
- {
- Arrays.fill(f[i], 0);
- }
- int r[][]=residualNetwork(graph,f);
- int result=augmentPath(r,s,t);
- int sum=0;
- while(result!=-1)
- {
- int cur=t;
- while(cur!=s)
- {
- f[parent[cur]][cur]+=result;
- f[cur][parent[cur]]=-f[parent[cur]][cur];
- r[parent[cur]][cur]-=result;
- r[cur][parent[cur]]+=result;
- cur=parent[cur];
- }
- sum+=result;
- result=augmentPath(r,s,t);
- }
- residualNetwork=r;
- flowNetwork=f;
- return sum;
- }
- /**
- * deepCopy
- * @param c
- * @param f
- * @return
- */
- private int[][] residualNetwork(int c[][],int f[][]) {
- int length=c.length;
- int r[][]=new int[length][length];
- for(int i=0;i<length;i++)
- {
- for(int j=0;j<length;j++)
- {
- r[i][j]=c[i][j]-f[i][j];
- }
- }
- return r;
- }
- /**
- * 广度优先遍历,寻找增光路径,也是最短增广路径
- * @param graph
- * @param s
- * @param t
- * @return
- */
- public int augmentPath(int graph[][],int s,int t)
- {
- int maxflow=Integer.MAX_VALUE;
- Arrays.fill(parent, -1);
- Queue<Integer> queue=new LinkedList<Integer>();
- queue.add(s);
- parent[s]=s;
- while(!queue.isEmpty())
- {
- int p=queue.poll();
- if(p==t)
- break;
- for(int i=0;i<graph.length;i++)
- {
- if(i!=p&&parent[i]==-1&&graph[p][i]>0)
- {
- if(maxflow>graph[p][i])
- maxflow=graph[p][i];
- //flow[i]=Math.min(flow[p], graph[p][i]);
- parent[i]=p;
- queue.add(i);
- }
- }
- }
- if(parent[t]==-1)
- return -1;
- return maxflow;
- }
- public int[][] getResidualNetwork() {
- return residualNetwork;
- }
- public int[][] getFlowNetwork() {
- return flowNetwork;
- }
- }
2. 求解最大流问题
2.1 枚举算法
时间复杂度: O(2^|V|·|E|)
思路:枚举所有割(S,T),找到容量c(S,T)最小的那个割的容量,即为最大流的流值
评价:算法复杂度高,因此仅当顶点个数|V|较少时适用(否则整数越界);另外,它还不能给出得到最大流的具体的网络流,只是返回了最大流值。
- //source在最低位,对应1;sink在最高位,对应0。
- //sink * * * * * * source
- // 0 0 0 0 0 0 0 1
- // 0 0 0 0 0 0 1 1
- // 0 . . . . . . 1 ==>每一种二进制组合对应一个割
- // 0 1 1 1 1 1 1 1
- // | |
- // ------ i -------
- //用int i枚举二进制组合
- int maxflow(){
- int flow=INF;
- int tmp;
- for(int i=0;i<(1<<n-2);i++){
- //枚举每一个割(S,T). S中元素对应位为1, T中元素对应为为0
- s=(i<<1)|1;
- tmp=0;
- for(int u=0;u<n;u++)
- for(int v=0;v<n;v++)
- if( ((s>>u)&1)==1 && ((s>>v)&1)==0 )
- tmp+=c[u][v];
- if(flow>tmp) flow=tmp;
- }
- return flow;
- }
2.2 增广路算法
FORD-FULKERSON方法:
- f=0
- while( p exists)
- do f+=p
其中,f是流值|f|不断增加的网络流,初始时f=0,之后不断沿着增广路径压入正网络流;
p是每次找到的增广路径
- FORD-FULKERSON(G,s,t)
- for each edge(u,v)∈E[G]
- do f[u,v] <- 0
- f[v,u] <- 0
- while there exists a path p from s to t in the residual network Gf
- do cf(p) <- min{cf(u,v): (u,v) is in p}
- for each edge(u,v) in p
- do f[u,v] <- f[u,v] + cf(p) //“沿着”增广路径,有向边上的流“增加”增广路径残留容量cf(p)
- f[v,u] <- - f[u,v] //“逆着”增广路径,有向边上的流“减少”增广路径残留容量cf(p)
B. BFS搜索增广路径 (即“Edmonds-Karp算法”)
时间复杂度:O(|V|·|E|^2) ==>每次搜索增广路径需要O(|E|);可以增广O(|V||E|)次,原因见下面评价
思路:每次BFS得到一条最短的增广路径增广
评价:经证明(independently proved by Dinic in 1970, and by Edmonds & Karp in 1972),若每次从最短增广路径开始增广,则最多只用进行O(|V||E|)次增广,而不是DFS搜索增广路径的O(c)次。其中“最短增广路径”指的是:当每条边长度都为1时的最短路径,显然,最朴素的想法就是用BFS来找这种最短路径。
BFS搜索增广路径——Edmonds-karp算法:- const int N=210;//边
- const int INF=0x7FFFFFFF;
- int n,m;
- int map[N][N]; //残留图,初始状态标识每条有向边的容量的图也可以看做是一个残留图
- int pi[N]; //BFS的前驱图; pi[start]=0;pi[i]=-1,如果i没有在BFS中被扩展过
- int flow_in[N]; //流入i的最大流量是flow_in[i]
- int start,end;
- queue<int> q;
- int bfs(){
- int i,t;
- while(!q.empty()) q.pop();
- memset(pi,-1,sizeof(pi));
- pi[start]=0;
- flow_in[start]=INF; //key!
- q.push(start);
- while(!q.empty()){
- t=q.front();
- q.pop();
- if(t==end) break;
- for(i=1;i<=m;i++){
- if(pi[i]==-1 && map[t][i]){
- flow_in[i]=flow_in[t]<map[t][i]?flow_in[t]:map[t][i];
- q.push(i);
- pi[i]=t;
- }
- }
- }
- if(pi[end]==-1) return -1; //不存在增广路径
- else return flow_in[m]; //还存在增光路径,不保证flow_in[m]达到最大可能值
- }
- int Edmonds_Karp(){
- int max_flow_in=0; //流f的流值|f|
- int cf_p; //增广路径的残留容量Cf(p)
- int now,pre;
- while((cf_p=bfs())!=-1){
- //1. 流值|f|增加本次增广路径的残留容量cf_p
- max_flow_in+=cf_p;
- //2. 顺着和逆着增广路径压入网络流
- now=end;
- while(now!=start){
- pre=pi[now];
- map[pre][now]-=cf_p; //更新正向边的实际容量
- map[now][pre]+=cf_p; //添加反向边
- now=pre;
- }
- }
- return max_flow_in;
- }
A. DFS搜索增广路径
时间复杂度:O(|E|·c) , c是最大流 ==>每次搜索增广路径需要O(|E|);可以增广O(c)次,∵每次可以增广大小为1的流
思路:每次DFS得到一条盲目的增广路径增广
评价:经证明,若每次从最短增广路径开始增广,则最多只用进行O(|V||E|)次增广,而不是DFS搜索增广路径的O(c)次。从这个意义上讲,DFS搜索增广路径是盲目的。
- int n; //有向边数
- int m; //顶点数
- int f[N][N]; //网络流
- int cf[N][N]; //残留网络
- bool vis[N]; //DFS中是否访问过
- const int inf = 1<<29;
- int inline min(int x,int y){return x>y?y:x;}
- bool dfs(int s,int t,int &cf_path)
- {
- vis[s]=true;
- if(s==t)
- return true;
- for(int i=1;i<=m;++i)
- {
- if(vis[i]==false && cf[s][i]>0)
- {
- int temp = cf_path;
- cf_path=min(cf_path,cf[s][i]); //(a)式
- if(dfs(i,t,cf_path))
- {
- f[s][i]+=cf_path;
- cf[s][i]-=cf_path;
- cf[i][s]+=cf_path; //允许“反悔”
- return true;
- }
- //沿着<s,i>向下深搜失败,回溯时恢复增广路径的残留容量cf[path];
- //实际上是在撤销(a)式的影响!
- cf_path = temp;
- }
- }
- return false;
- }
- int find(int s=1,int t=m) //这实际上是DFS的外围框架(回忆DFS算法实现有内外两层)
- {
- int cf_path = inf; //本次可增广的流量,在DFS过程中被限制得越来越小
- memset(vis,0,sizeof(vis));
- dfs(s,t,cf_path);
- if(cf_path>=inf)
- return 0;
- else
- return cf_path;
- }
- int max_flow()
- {
- int ret=0,i;
- memset(f,0,sizeof(f));
- while(find());
- for(i=1;i<=m;++i)
- ret+=f[1][i]; //网络流的值=从源出发的流量和f(s,V)=流入汇点的流量和f(V,t)
- return ret;
- }
SAP算法:
- struct sap
- {
- int s,t; //s:源点 t:汇点
- int m; //顶点数
- int cf[305][305]; //残留容量
- int flow; //当前最大流
- int cf_path; //本次可增广的流量(本次增广路径的残留容量)
- bool flag; //本次增广路径是否找到
- int vh[1000]; //各高度的顶点个数
- int h[1000]; //各顶点的高度,又称为“距离标号”——到汇点距离的下界(边权值都为1)
- sap(){
- flow =cf_path =s =t =0;
- flag=false;
- memset(cf,0,sizeof(cf));
- memset(vh,0,sizeof(vh));
- memset(h,0,sizeof(h));
- }
- /*find_path_sap(int cur):增广路径已经推进到顶点cur,尝试向下一个顶点推进
- 主要思路:
- 递归地尝试沿着当前点cur的所有允许弧推进增广路径。
- 如果失败,将当前点cur的高度(距离函数下界)更新到minH(minH:残留网络中所有cur指向节点的最小高度)
- */
- void find_path_sap(int cur){
- //1. 检查是否走到增广路径末端
- //*a. 终止条件:cur是本次增广路径末端节点(汇点)
- if(cur==t){
- flow+=cf_path;
- flag=true;
- return;
- }
- //2. 尝试递归/回溯 找增广路径
- //如果cur仅比邻接点高1,则尝试最大限度向它送出流
- int i;
- int minH=m-1;
- int tmp_cf_path=cf_path;
- for(i=1;i<=m;i++){
- if(cf[cur][i]){
- if(h[i]+1==h[cur]){
- if(cf[cur][i]<cf_path)
- cf_path=cf[cur][i];
- find_path_sap(i);
- //*b. 终止条件: 如果h[1]增长到m,表示通过本次"find_path_sap(i)"递归搜索,不能找到一条增广路径
- if(h[1]>=m)
- return;
- //*c. 终止条件: 通过本次"find_path_sap(i)"递归搜索,找到了一条增广路径
- if(flag)
- break;
- //通过本次"find_path_sap(i)"递归没有找到一条增广路径,回溯时需要恢复状态
- cf_path=tmp_cf_path;
- }
- if(h[i]<minH)
- minH=h[i];
- }
- }
- //以上for()执行完,minH为cur的邻接顶点中高度最小的点的高度
- //3. 检查回溯结果
- // 成功:逆着增广路径压入增广的流
- // 失败:顶点cur高度增加1
- if(flag){
- //上面代码中的某次递归成功找到一条增广路径(从cur找下去,找到一条增广路径)
- cf[cur][i]-=cf_path;
- cf[i][cur]+=cf_path;
- }else{
- vh[h[cur]]--;
- if(vh[h[cur]]==0)
- h[1]=m; //作用到终止条件"*b"
- h[cur]=minH+1;
- vh[h[cur]]++;
- }
- }
- //Ford-Fulkerson算法框架
- int solve(){
- vh[0]=m;
- flow=0;
- //h[1]保持<m,一旦增长到m,就不再存在任何增广路径
- while(h[1]<m)
- {
- flag=false;
- cf_path=INF;
- find_path_sap(s);
- }
- return flow;
- }
- void addEdge(int x,int y,int c){
- cf[x][y]+=c;
- }
- };
- int main(){
- int n; //有向边数
- int m; //顶点数
- while(scanf("%d %d",&n,&m)!=-1)
- {
- sap nt= sap();
- nt.s=1;
- nt.t=m;
- nt.m=m;
- for(int i=1;i<=n;i++)
- {
- int x,y,c;
- scanf("%d %d %d",&x,&y,&c);
- nt.addEdge(x,y,c);
- }
- printf("%d\n",nt.solve());
- }
- return 0;
- }
C. SAP (shortest augmenting path)
时间复杂度:O(|V|^2·|E|) ==>每次搜索增广路径需要O(|V|);可以增广O(|V||E|)次,原因见下面评价
思路:每次"沿着从源到汇的允许弧(必然是源到汇的最短路径)"得到一条最短的增广路径增广。
允许弧:如果一条弧(u,v)满足h[u]=h[v]+1 ,即前点仅比后点高1
评价:经证明(independently proved by Dinic in 1970, and byEdmonds & Karp in 1972),若每次从最短增广路径开始增广,则最多只用进行O(|V||E|)次增广,而不是DFS搜索增广路径的O(c)次。本质类似"BFS搜索增广路径",只不过找最短路径的方法不同。再次注意,其中“最短增广路径”指的是:当每条边长度都为1时的最短路径
Learn more:
1. Dinic算法
2. 预流推进算法
Also check http://community.topcoder.com/tc?module=Static&d1=tutorials&d2=maxFlowRevisited