网络流【最大流&&最小割&&费用流】——一篇简单易懂的博文

网络流算法


一.网络流

我们先来了解什么是网络流。

网络流(network-flows)是一种类比水流的解决问题方法,与线性规划密切相关。网络流的理论和应用在不断发展,出现了具有增益的流、多终端流、多商品流以及网络流的分解与合成等新课题。网络流的应用已遍及通讯、运输、电力、工程规划、任务分派、设备更新以及计算机辅助设计等众多领域。

简单点说,就像你家的水管,从自来水公司到你家的水龙头,水管就像一张网一样分布,水管有大有小。然后出现了一系列问题(我也是很崩溃,为什么有那么多问题啊!)

网络流是图论中的一种理论与方法,研究网络上的一类最优化问题。

1955年 ,T.E.哈里斯在研究铁路最大通量时首先提出在一个给定的网络上寻求两点间最大运输量的问题。

1956年,L.R. 福特D.R. 富尔克森等人给出了解决这类问题的算法,从而建立了网络流理论。他们指出最大流的流值等于最小割(截集)的容量这个重要的事实,并根据这一原理设计了用标号法求最大流的方法,后来又有人加以改进,使得求解最大流的方法更加丰富和完善 。最大流问题的研究密切了图论和运筹学,特别是与线性规划的联系,开辟了图论应用的新途径。

Orz大佬们,这种算法都想的出来。

二.网络流的基本性质

我们先定义C[u,v]F[u,v],C[u,v]表示连接(u,v)这个水管的容量,F[u,v]表示连接(u,v)水管的流量。输入水的点称为原点,记作S;输出水的点称为汇点,记作T。

1.F[u,v]≤C[u,v] 很显然,如果F>C水管就炸了~~

2.流量守恒,就像化学反应一样,不会平白无故的多出或少掉一些,也就是说只能在源头输入水。做一个形象的比喻,你家的水管断掉了,维修人员还没有来,但是你打开水龙头,里面却有水出来,是不是很恐怖啊?当然,源点输入多少水,最后汇点收到多少水。

3.F[u,v]=-F[v,u]你可以理解成位移,是有方向的。打个比方,你从u往v输入 5 c m 3 5 cm^3 5cm3水量,然后又从v往u输入 3 c m 3 3 cm^3 3cm3水量,也就等于从u往v输入 2 c m 3 2 cm^3 2cm3水量。

4.容量=流量+残量 容量,流量,残量是重要的变量,可以通过字面理解。

三.最大流问题

先看洛谷的一道模板题洛谷 P3376 【模板】网络最大流

如图1-1,给你一个容量网络,假设要把一些物品从S送到T,问你T最多能收到多少物品。

1-1

图1-2,展示了一种可行方案,第一个数字表示流量,第二个表示容量。

在这里插入图片描述

这样的问题称为最大流问题(Maximum-Flow Problem)

我们求解最大流问题有两种算法Edmonds-KarpDinic算法,利用增广路来求解。

1.Edmonds-Karp

我们先讲Edmonds-Karp算法,简称EK(我认为这种算法比Dinic慢)

如图1-3(红色表示当前选的路径),这个想法就是先找一条从S到T的可行路径,然后往这条路径上输入X个流量,X取决于这条路径上最小的流量(毋庸置疑,否则就将管道挤爆了)。

在这里插入图片描述

只要找到一条可行路径,就可以停下了。这时有人会问,如果这条路径的不是最理想的怎么办。这就用到反向的边了,如果有一条更优秀的,那么就会往回流,避免了这种情况,也就是网络流里很重要的一点。(想到这里,不禁开始膜拜那些大神了,这么优秀的方法都想的出来。Orz)

通过以上简单的讲述,这个算法基本讲完了,不过是多次BFS,然后将这条路径上所有的流量增加X。这就是Edmonds-Karp

下面给出核心代码:

MAXN//点数
MAXE//边数 
struct ad(){
	int x,y,C,F;//表示从x到y的容量是C,流量是F。
}a[2*MAXE];//因为有反向边,所以边数*2
vector<int> G[MAXN];//建立边表,存的是[x,y]这条边在a中的ID
int que[MAXN];//队列
int upd[MAXN];//用于更新当前最小的残量,如果upd[x]=0,表示这个点没被遍历过。
int fa[MAXN];//存储路径,从fa[y]走向y,方便增加流量

void Add(int x,int y,int c,int i){//i从1~MAXE
	a[i]=xcw(x,y,c,0);G[x].push_back(i-1<<1);
	a[i]=xcw(y,x,0,0);G[y].push_back((i-1<<1)+1);
}
bool BFS(){
	hd=0;que[tl=1]=S;
	memset(upd,0,sizeof(upd));upd[S]=1e9;
	while(hd!=tl){
		int x=que[++hd],L=G[x].size();
		for(int j=0;j<L;j++){
			xcw E=a[G[x][j]];
			if(!upd[E.y]&&E.C>E.F) 
			upd[E.y]=min(upd[x],E.C-E.F),que[++tl]=E.y,fa[E.y]=G[x][j];
		}
		if(upd[T]) return 1;//如果走到T就返回真值
	}
	return 0;//没走到返回假值
}
int Edmonds_Karp(){
	int Flow=0;
	while(BFS()){
		for(int x=T;x^S;x=a[fa[x]].x)//枚举当前BFS走的路径
		a[fa[x]].F+=upd[T],//加上当前最小残量
		a[fa[x]^1].F-=upd[T];//相反边减去最小残量
		Flow+=upd[T];
	}
	return Flow;//返回答案
}

一个问题供大家思考:为什么a[i^1]就是i这条边的相反边?

2. Dinic

这种算法其实和EK同一个原理,但是Dinic的更新过程却是不同的。

比如说是这样的一幅图:

2-1

如果用EK求解,那么就会成这样:

2-2

2-3

就会这么一直下去,如果在大一点,那么就会超时。

但是Dinic就能一步到位,只需一次DFS过程就可以实现多次增广(核心),他记录了当前节点的深度,用DFS去修正,但是在一定情况下又比EK慢,应为DFS修正也需要时间。

下面给出一段Dinic的代码:

#include<cstdio>
#include<cstring>
#include<iostream>
#define MAXN 10005
#define MAXE 200005
using namespace std;
int n,m,S,T,hd,tl,que[MAXN],dep[MAXN];
int tot=-1,lnk[MAXN],nxt[MAXE],son[MAXE],C[MAXE],F[MAXE];
int read(){
	int ret=0;char ch=getchar();bool f=1;
	for(;!isdigit(ch);ch=getchar()) f^=!(ch^'-');
	for(; isdigit(ch);ch=getchar()) ret=(ret<<3)+(ret<<1)+ch-48;
	return f?ret:-ret;
}
void add(int x,int y,int c,int f){son[++tot]=y;nxt[tot]=lnk[x];lnk[x]=tot;C[tot]=c;F[tot]=f;}
bool BFS(){
	hd=0;que[tl=1]=S;
	memset(dep,0,sizeof(dep));dep[S]=1;//更新点深度,可以说是刷层次 
	while(hd!=tl){
		int x=que[++hd];
		for(int j=lnk[x];j!=-1;j=nxt[j])
		if(!dep[son[j]]&&C[j]>F[j]) dep[son[j]]=dep[x]+1,que[++tl]=son[j];
	}
	return dep[T];
}
int DFS(int x,int flow){
	if(x==T) return flow;//到T点就停 
	int now=0;
	for(int j=lnk[x];j!=-1;j=nxt[j])
	if(dep[x]+1==dep[son[j]]&&C[j]>F[j]){
		int y=DFS(son[j],min(flow,C[j]-F[j]));//选出最小流量 
		if(y>0){F[j]+=y;F[j^1]-=y;now+=y;flow-=y;if(!flow) return now;}//进行更新 
	}
	return now;
}
int Dinic(){
	int ans=0;
	while(BFS())
	while(int t=DFS(S,1e9)) ans+=t;
	return ans;
}
int main(){
	memset(lnk,-1,sizeof(lnk));
	n=read(),m=read();S=read(),T=read();
	for(int i=1;i<=m;i++){
		int x=read(),y=read(),z=read();
		add(x,y,z,0);add(y,x,z,z);
	}
	printf("%d\n",Dinic());
	return 0;
}

四、最大流最小割

最大流最小割定理:网络的最大流等于最小割

是不是很懵逼。

我在网上看到一个人写的很好,给大家观摩一下。

具体的证明分三部分

1.任意一个流都小于等于任意一个割

这个很好理解 自来水公司随便给你家通点水,构成一个流
恐怖分子随便砍几刀 砍出一个割
由于容量限制,每一根的被砍的水管子流出的水流量都小于管子的容量
每一根被砍的水管的水本来都要到你家的,现在流到外面 加起来得到的流量还是等于原来的流
管子的容量加起来就是割,所以流小于等于割
由于上面的流和割都是任意构造的,所以任意一个流小于任意一个割

2.构造出一个流等于一个割

当达到最大流时,根据增广路定理
残留网络中s到t已经没有通路了,否则还能继续增广
我们把s能到的的点集设为S,不能到的点集为T
构造出一个割集C[S,T],S到T的边必然满流 否则就能继续增广
这些满流边的流量和就是当前的流即最大流
把这些满流边作为割,就构造出了一个和最大流相等的割

3.最大流等于最小割

设相等的流和割分别为Fm和Cm
则因为任意一个流小于等于任意一个割

任意F≤Fm=Cm≤任意C

定理说明完成,证明如下:

对于一个网络流图 G = ( V , E ) G=(V,E) G=(V,E),其中有源点s和汇点t,那么下面三个条件是等价的:

  1. 流f是图G的最大流
  2. 残留网络Gf不存在增广路
  3. 对于G的某一个割(S,T),此时 f = C ( S , T ) f = C(S,T) f=C(S,T)

首先证明1 => 2

我们利用反证法,假设流f是图G的最大流,但是残留网络中还存在有增广路p,其流量为fp。则我们有流 f ′ = f + f p &gt; f f&#x27;=f+fp&gt;f f=f+fp>f。这与f是最大流产生矛盾。

接着证明2 => 3

假设残留网络Gf不存在增广路,所以在残留网络Gf中不存在路径从s到达t。我们定义S集合为:当前残留网络中s能够到达的点。同时定义T=V-S。
此时(S,T)构成一个割(S,T)。且对于任意的 u ∈ S , v ∈ T u∈S,v∈T uS,vT,有 f ( u , v ) = c ( u , v ) f(u,v)=c(u,v) f(u,v)=c(u,v)。若 f ( u , v ) &lt; c ( u , v ) f(u,v)&lt;c(u,v) f(u,v)<c(u,v),则有 G f ( u , v ) &gt; 0 Gf(u,v)&gt;0 Gf(u,v)>0,s可以到达v,与v属于T矛盾。
因此有 f ( S , T ) = Σ f ( u , v ) = Σ c ( u , v ) = C ( S , T ) f(S,T)=Σf(u,v)=Σc(u,v)=C(S,T) f(S,T)=Σf(u,v)=Σc(u,v)=C(S,T)

最后证明3 => 1

由于f的上界为最小割,当f到达割的容量时,显然就已经到达最大值,因此f为最大流。

这样就说明了为什么找不到增广路时,所求得的一定是最大流。

本来以为最小割很难,是我的书惹的祸,原来是这样子的啊!如果看不懂,那么你只要记住这个定理就可以了。

费用流

一年后才写费用流,很对不起大家。

顾名思义,费用流指的是有边权的流,就是一条边既有流量,也有单位流量的代价。

其实很简单,我们最大流时每次BFS去找边,费用流时就用最短路找,是不是很简单啊。

但是要注意,建回流的边的时候边权为相反数。

最大权闭合子图

经典的最大流问题,什么是最大权闭合子图呢?就是给你一个带权图,我们要取出一个连通块使之权值最大。

对于这种题目我们就建 ( s , i ) (s,i) (s,i)费用为 a [ i ] a[i] a[i] a [ i ] &gt; 0 a[i]&gt;0 a[i]>0时,否则建 ( i , t ) (i,t) (i,t)费用为 − a [ i ] -a[i] a[i] a [ i ] &lt; 0 a[i]&lt;0 a[i]<0时。

给出一段代码,我忘了题目描述QAQ

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int MAXN=105;
int n,m,T,s,t,Top,cur[MAXN],a[MAXN],Dep[MAXN],Ans,Sum;
struct Edge{
	int tot,lnk[MAXN],son[MAXN<<1],nxt[MAXN<<1];
	void clear(){memset(lnk,0,sizeof(lnk));tot=0;}
	void Add(int x,int y){nxt[++tot]=lnk[x];lnk[x]=tot;son[tot]=y;}
}S1,S2;
struct Flow{
	int tot,lnk[MAXN],son[MAXN<<3],nxt[MAXN<<3],C[MAXN<<3];
	void clear(){memset(lnk,-1,sizeof(lnk));tot=-1;}
	void Add(int x,int y,int c){nxt[++tot]=lnk[x];lnk[x]=tot;son[tot]=y;C[tot]=c;}
	void Add_E(int x,int y,int c){Add(x,y,c),Add(y,x,0);}
}E;
void DFS1(int x,int fa){
	if(fa) E.Add_E(x,fa,1<<30);
	for(int j=S1.lnk[x];j;j=S1.nxt[j]) if(S1.son[j]!=fa) DFS1(S1.son[j],x);
}
void DFS2(int x,int fa){
	if(fa) E.Add_E(x,fa,1<<30);
	for(int j=S2.lnk[x];j;j=S2.nxt[j]) if(S2.son[j]!=fa) DFS2(S2.son[j],x);
}
int hd,tl,que[MAXN];
bool BFS(){
	for(int i=s;i<=t;i++) Dep[i]=0;
	hd=0;que[tl=1]=s;Dep[s]=1;
	while(hd^tl){
		int x=que[++hd];
		for(int j=E.lnk[x];j^-1;j=E.nxt[j])
		if(!Dep[E.son[j]]&&E.C[j]) Dep[E.son[j]]=Dep[x]+1,que[++tl]=E.son[j];
	}
	return Dep[t];
}
int DFS(int x,int flow){
	if(x==t) return flow;
	for(int &j=cur[x],y;j^-1;j=E.nxt[j])
	if(Dep[x]+1==Dep[E.son[j]]&&E.C[j]>0&&(y=DFS(E.son[j],flow<E.C[j]?flow:E.C[j]))>0){E.C[j]-=y,E.C[j^1]+=y;return y;}
	return 0;
}
int Dinic(){
	int Sum=0;
	while(BFS()){
		for (int i=0;i<=t;i++) cur[i]=E.lnk[i];
		while(int Add=DFS(s,1<<30)) Sum+=Add;
	}
	return Sum;
}
int main(){
	freopen("theory.in","r",stdin);
	freopen("theory.out","w",stdout);
	scanf("%d",&T);
	while(T--){
		scanf("%d",&n);S1.clear(),S2.clear();Ans=0;Sum=0;
		for(int i=1;i<=n;i++) scanf("%d",&a[i]),Sum+=(a[i]>0?a[i]:0);
		for(int i=1,x,y;i<n;i++) scanf("%d%d",&x,&y),S1.Add(x,y),S1.Add(y,x);
		for(int i=1,x,y;i<n;i++) scanf("%d%d",&x,&y),S2.Add(x,y),S2.Add(y,x);
		s=0,t=n+1;
		for(int i=1;i<=n;i++){
			E.clear();DFS1(i,0);DFS2(i,0);
			for(int j=1;j<=n;j++) if(a[j]>0) E.Add_E(s,j,a[j]);else E.Add_E(j,t,-a[j]);
			Ans=max(Ans,Sum-Dinic());
		}
		printf("%d\n",Ans);
	}
	return 0;
}
  • 35
    点赞
  • 121
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值