网络流总结(一)最大流

   网络流的相关定义:

  • 源点:有n个点,有m条有向边,有一个点很特殊,只出不进,叫做源点
  • 汇点:另一个点也很特殊,只进不出,叫做汇点
  • 容量和流量:每条有向边上有两个量,容量和流量,从i到j的容量通常用c[i,j]表示,流量则通常是f[i,j].

通常可以把这些边想象成道路,流量就是这条道路的车流量,容量就是道路可承受的最大的车流量。很显然的,流量<=容量。而对于每个不是源点和汇点的点来说,可以类比的想象成没有存储功能的货物的中转站,所有“进入”他们的流量和等于所有从他本身“出去”的流量。

  • 最大流:把源点比作工厂的话,问题就是求从工厂最大可以发出多少货物,是不至于超过道路的容量限制,也就是,最大流
  • 残量(残余容量):每条边中容量与流量的差
  • 反相边:若从点u到v的边容量为c,这条边上有流量f流过(称为正向边),则相当于从v到u有一条容量为0的边,其流量为-f,这条边是反向边。(作用:在有更优决策时,撤销已选的边) 
  • 残量网络:计算图中每条边的残量得到的网络,称为残量网络,并称残量网络上的s-t路径增广路。 
  • 增广: 残量网络中任何一条从s到t的有向道路都对应一条原图中增广路——只要求出该道路中所有残量的最小值d,把对应的所有边上的流量增加d即可,这个过程称为增广。显然,只要残量网络中存在增广路,流量就可以增大。可以证明它的逆命题也成立:如果残量网络中不存在增广路,则当前流就是最大流。这就是著名的增广路定义

  最大流的问题中,容量c和流量f满足的3个性质:

  • 容量限制:f(u,v)<=c(u,v)
  • 斜对称性:f(u,v)=-f(v,u) 
  • 流量平衡:从s点流出的净流量=流入t点的净流量 

    反相边的作用: (此部分摘抄https://www.cnblogs.com/ZJUT-jiangnan/p/3632525.html

举例:

比如说下面这个网络流模型

3

我们第一次找到了1-2-3-4这条增广路,这条路上的delta值显然是1。

于是我们修改后得到了下面这个流。(图中的数字是容量)

4

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

但是,

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

那么我们刚刚的算法问题在哪里呢

问题就在于我们没有给程序一个“后悔”的机会,应该有一个不走(2-3-4)而改走(2-4)的机制。

那么如何解决这个问题呢

我们利用一个叫做反向边的概念来解决这个问题。即每条边(i,j)都有一条反向边(j,i),反向边也同样有它的容量。

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

在第一次找到增广路之后,在把路上每一段的容量减少delta的同时,也把每一段上的反方向的容量增加delta。
我们来看刚才的例子,在找到1-2-3-4这条增广路之后,把容量修改成如下:

1

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

2

那么,这么做为什么会是对的呢?

事实上,当我们第二次的增广路走3-2这条反向边的时候,就相当于把2-3这条正向边已经是用了的流量给“退”了回去,不走2-3这条路,而改走从2点出发的其他的路也就是2-4。

如果这里没有2-4怎么办?

这时假如没有2-4这条路的话,最终这条增广路也不会存在,因为他根本不能走到汇点

同时本来在3-4上的流量由1-3-4这条路来“接管”。而最终2-3这条路正向流量1,反向流量1,等于没有流。

   

最小割最大流定理:

  • s-t割:源点s在集合S中,汇点t在集合T中,起点在S中,终点在T中的集合(S,T)称为s-t割,容量定义为c(S,T)=\sum c(u,v)(u在S中,v在T中)。
  • 最小割:如果将网络中的s-t割包含的边都删去,也就不再有从s到t的路径啦,为保证没有从s到t的路径,需要删去的边的总容量的最小值称为最小割
  • 最小割最大流定理:最小割等于最大流。

最大流模板题:

poj1273 - Drainage Ditches

Sample Input

5 4    //5条边,4个点,求从1点到4点的最大流
1 2 40 //点1到点2的容量是40
1 4 20
2 4 20
2 3 30
3 4 10

Sample Output

50

EDMONDS-KARP算法(最短增广路算法):

我的理解是:每执行一次bfs增广后,找到最短增广路径,以及对应的把增广值加到对应的增广路径上。

之后再不断地重新启动bfs从源点寻找另一条增广路。

当a[t]==0即从起点到终点的残量值为0时,(不再有增广路了)退出循环。

模拟一下这道题:

  • 初始状态,标注为容量:

  • 第一次寻找增广路s->t,要增广的值为20,此时图为

  • 第二次寻找增广路s->2->t,要增广的值为20,此时图为

  • 第三次寻找增广路s->2->3->t,要增广的值为10,此时图为

  • 此时无法再找出增广路,结束,答案为50,此时状态如图,标出的是各个边(容量-流量)之值

解释见注释

#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<algorithm>
#include<cmath>
#include<queue> 
 
using namespace std;

struct Edge{
	int from,to,cap,flow;
	Edge(int u,int v,int c,int f):from(u),to(v),cap(c),flow(f){}
};
const int INF=99999999,maxn=210;
int V,E,S,T;
vector<Edge> edges;//存正边和反向边,边数的两倍 
vector<int> G[maxn];//邻接表,G[i][j]表示节点i的第j条边在edges数组中的序号 
int a[maxn];//a[i]表示源点s到节点i的路径上的最小残留量 
int p[maxn];//p[i]记录i的前驱,是用在edges数组里的序号表示的 

void init(int n){
	for(int i=0;i<=n;i++){
		G[i].clear();
	}
    edges.clear();
}

void AddEdge(int from,int to,int cap){
	edges.push_back(Edge(from,to,cap,0));
	edges.push_back(Edge(to,from,0,0));//反向弧 
	int m=edges.size();
	G[from].push_back(m-2);
	G[to].push_back(m-1);
}

int Maxflow(int s,int t){
	int flow=0;
	for(;;){
		memset(a,0,sizeof(a));
		queue<int> q;
		q.push(s);
		a[s]=INF;
		while(!q.empty()){
			int x=q.front();q.pop();
			for(int i=0;i<G[x].size();i++){//x点对应的所有正向弧和反向弧 
				Edge& e=edges[G[x][i]];
				if(!a[e.to]&&e.cap>e.flow){//当这个点没被访问过,且容量大于流量时
					p[e.to]=G[x][i];
					a[e.to]=min(a[x],e.cap-e.flow);
					q.push(e.to);
				}
			}
			if(a[t])break;//a[t]初始为0,若此处不为0说明找到了增广路了,break
		}
		if(!a[t])break;//如果找不到增广路,则当前流已经是最大流
		for(int u=t;u!=s;u=edges[p[u]].from){
			edges[p[u]].flow+=a[t];//更新正向流量 
			edges[p[u]^1].flow-=a[t];//更新反向流量 
			/*
			异或运算,(^)向异为1,相同为0;
			正向弧和反向弧保存在一起,eg、0,1;2,3…
			一个数与1做异或运算,则其二进制除了最后一位,不变;
			最后一位是1(奇数),则^1后为0,相当于值-1 (比如3对应2) 
			最后一位是0(偶数),则^1后为1,相当于值+1 (比如4对应5)
			*/
		} 
		flow+=a[t];//流加上 
	} 
	return flow;
} 

int main(){
	int x1,x2,x3;
	while(scanf("%d%d",&E,&V)!=EOF){
		S=1;
		T=V;
		init(V);
		for(int i=0;i<E;i++){
			scanf("%d%d%d",&x1,&x2,&x3);
			AddEdge(x1,x2,x3);
		}
		printf("%d\n",Maxflow(S,T));
	} 
	
}

Dinic算法:

EK算法,每个阶段执行完一次bfs增广后,要重新启动bfs从源点寻找一条增广路,Dinic算法只需一个dfs就可实现多次增广,Dinic算法引入了一个叫做分层图的概念。具体就是对于每一个点,我们根据从源点开始的bfs序列,为每一个点分配一个深度,然后我们进行若干遍dfs寻找增广路,每一次由u推出v必须保证v的深度必须是u的深度+1。下面给出代码

#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<algorithm>
#include<cmath>
#include<queue> 
 
using namespace std;

const int MAX_V=205,INF=99999999;
int V,E,S,T;

struct edge{
    int to,cap,rev;//终点、容量、反相边 
};

vector<edge> G[MAX_V];//图的邻接表表示
int level[MAX_V];//顶点到源点的距离标号
int iter[MAX_V];//当前弧,在其之前的边已经没有用了 


void init(int n){
	for(int i=0;i<=n;i++){
		G[i].clear();
	}
}

void AddEdge(int from,int to,int cap){
	G[from].push_back((edge){to,cap,int(G[to].size())});
	G[to].push_back((edge){from,0,int(G[from].size())-1});
}

void bfs(int s){
	memset(level,-1,sizeof(level));
	queue<int> que;
	level[s]=0;
	que.push(s);
	while(!que.empty()){
		int v=que.front();que.pop();
		for(int i=0;i<G[v].size();i++){
			edge &e=G[v][i];
			if(e.cap>0&&level[e.to]<0){
				level[e.to]=level[v]+1;
				que.push(e.to);
			}
		}
	}
}

int dfs(int v,int t,int f){
	if(v==t)return f;
	for(int &i=iter[v];i<G[v].size();i++){
		edge &e=G[v][i];
		if(e.cap>0&&level[v]<level[e.to]){
			int d=dfs(e.to,t,min(f,e.cap));
			if(d>0){
				e.cap-=d;
				G[e.to][e.rev].cap+=d;
				return d; 
			}
		}
	}
	return 0;
}

int max_flow(int s,int t){
	int flow=0;
	for(;;){
		bfs(s);
		if(level[t]<0)return flow;
		memset(iter,0,sizeof(iter));
		int f;
		while((f=dfs(s,t,INF))>0){
			flow+=f;
		}
	}
}

int main(){
	int x1,x2,x3;
	while(scanf("%d%d",&E,&V)!=EOF){
		S=1;
		T=V;
		init(V);
		for(int i=0;i<E;i++){
			scanf("%d%d%d",&x1,&x2,&x3);
			AddEdge(x1,x2,x3);
		}
		printf("%d\n",max_flow(S,T));
	} 
}

猜你喜欢

转载自blog.csdn.net/m0_37579232/article/details/81287488
今日推荐