最大流

博客分类: 数据结构和算法 
思考顺序: 
先有 容量限制 c[] --> 有 多种 可能的 网络流 f[] --> 找 最大流 (流值|f|最大的流) 

一、网络流 

G=<V,E>为有向图 

1. 容量为非负: 如果有向边(u,v) 存在,c(u,v)≥0; 如果有向边(u,v)不存在,c(u,v)=0 

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)=0,即流入一个顶点的总流为0. 

引理26.1: G=<V,E>是网络流,f是G的一个流。 
(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) 

注意:定义f(X,Y)=∑f(x,y),x∈X, y∈Y. 

二、流值 和 最大流 

1. 流f的值|f|=f(s,V)=f(V,t): 源s送出多少水 /汇t喝了多少水 
2. 最大流问题: 给出流网络G(源点s,汇点t),希望从中找出流值|f*|最大的流f*, f*称为最大流 

三、最大流问题求解 

1. 三个理论基础 和 “最大流最小割定理”: 

理论一:“残留网络 residual network” 
网络流图G ==决定唯一==> 有向边的 残留容量 cf(u,v)=c(u,v)-f(u,v) 
网络流图G ==决定唯一==> G导出 的残留网络 Gf=<V,Ef>, Ef={(u,v)∈V×V: cf(u,v)>0} 
(1)后者(残留网络)由满足条件的前者(残留容量严格>0的有向边)组成 
(2)残留网络中的所有有向边上的残留容量均>0. 如果认为残留容量cf(u,v)是残留网络的(有向)边的权值,则所有权值严格>0,∵不满足介个条件的有向边根本就通不过海选! 

理论二:“增广路径 augmenting path” 
增广路径 p: 残留网络Gf中从源点s到汇点t的一条简单路径。 
增广路径p的残留容量 : cf(p)=min{cf(u,v): (u,v)在增广路径p上} 

理论三:“割 cut” 
网络流G的割(S,T),源点s∈S, 汇点t∈T。从S到T的边称为割边。 

最大流最小割定理 : 
(1)f是G的一个最大流 ==>达到流值|f|=f(s,V)最大 
(2)残留网络Gf不包含增广路径 ==>不能再压入正网络流 
(3)对G的某个割(S,T) ,存在|f|=c(S,T) ==>对最大流的限制来自最小容量的那个割 

2. 求解最大流问题 

2.1 枚举算法 
时间复杂度: O(2^|V|·|E|) 
思路:枚举所有割(S,T),找到容量c(S,T)最小的那个割的容量,即为最大流的流值 
评价:算法复杂度高;仅当顶点个数|V|较少时适用(否则整数越界) 
Cpp代码 
//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 

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算法: 
伪代码代码 
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) 

A. DFS搜索增广路径 
时间复杂度:O(|E|·c) , c是最大流 ==>每次搜索增广路径需要O(|E|);可以增广O(c)次,∵每次可以增广大小为1的流 
思路:每次DFS得到一条盲目的增广路径增广 
评价:经证明,若每次从最短增广路径开始增广,则最多只用进行O(|V||E|)次增广,而不是DFS搜索增广路径的O(c)次。从这个意义上讲,DFS搜索增广路径是盲目的。 

伪代码代码 
用FORD-FULKERSON算法,每次用DFS搜索增广路径(见"四、举例") 

B. BFS搜索增广路径 (即“Edmonds-Karp算法”) 
时间复杂度:O(|V|·|E|^2) ==>每次搜索增广路径需要O(|E|);可以增广O(|V||E|)次,原因见下面评价 
思路:每次BFS得到一条最短的增广路径增广 
评价:经证明(independently proved by Dinic in 1970, and byEdmonds & Karp in 1972),若每次从最短增广路径开始增广,则最多只用进行O(|V||E|)次增广,而不是DFS搜索增广路径的O(c)次。其中“最短增广路径”指的是:当每条边长度都为1时的最短路径,显然,最朴素的想法就是用BFS来找这种最短路径。 
伪代码代码 
用FORD-FULKERSON算法,每次用BFS搜索增广路径(见"四、举例") 

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时的最短路径 
伪代码代码 
用FORD-FULKERSON算法,每次用“沿着源到汇的允许弧”得到增广路径(见"四、举例") 

SAP算法我觉得很不好理解,因此专门用一节“最大流(二)——SAP算法”来讨论这个问题

D. Dinic 
时间复杂度: 
思路: 
评价: 



2.3 预流推进算法(即《算法导论》26.4 压入和重标记算法) 

四、举例——求解最大流 

这道题是纯粹的最大流问题,没有其他任何背景。下面几道题均在POJ里AC了的:)参考各种牛人代码,鞠躬! 
Drainage Ditches ==>  http://poj.org/problem?id=1273  

DFS搜索增广路径: 
Cpp代码 
#include <cstdio> 
#include <cstring> 
#define N 201 

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; 


int main(void) 

int i,x,y,z; 
while(scanf("%d%d",&n,&m)!=EOF) 

memset(cf,0,sizeof(cf)); 
for(i=0;i<n;++i) 

scanf("%d%d%d",&x,&y,&z); 
cf[x][y]+=z; 

printf("%d\n",max_flow()); 

return 0; 


BFS搜索增广路径——Edmonds-karp算法: 
Cpp代码 
#include<iostream> 
#include<queue> 
using namespace std; 

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; 


int main(){ 
int i,u,v,cost; 
while(scanf("%d%d",&n,&m)!=EOF){ 
memset(map,0,sizeof(map));//对于int[]/int[][],memset只能正常赋予0,-1两个值 
for(i=0;i<n;i++){ 
scanf("%d%d%d",&u,&v,&cost); 
map[u][v]+=cost; 

start=1,end=m; 
printf("%d\n",Edmonds_Karp()); 

return 0; 


SAP算法: 
Cpp代码 
#include<stdio.h> 
#include<string.h> 
#define INF 1<<29; 

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]; //各顶点的高度 

sap(){ 
flow =cf_path =s =t =0; 
flag=false; 
memset(cf,0,sizeof(cf)); 
memset(vh,0,sizeof(vh)); 
memset(h,0,sizeof(h)); 


//sap: 尝试找到一个最短的增广路径 
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; 




参考资料: 
另一个小盆友学习的心得:  http://hi.baidu.com/sheep_finalfreedom/blog/item/273772dc2cf5775994ee3795.html#0  
外国大牛的文章: http://community.topcoder.com/tc?module=Static&d1=tutorials&d2=maxFlowRevisited#1  

???待解决问题: 
1. Dinic算法 
2. 预流推进算法
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值