Typesetting math: 100%

网络流

网络流

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

最大流#

定义#

管道网络中每条边的最大通过能力(容量)是有限的,实际流量不超过容量。
最大流问题(maximum flow problem),一种组合最优化问题,就是要讨论如何充分利用装置的能力,使得运输的流量最大,以取得最好的效果。求最大流的标号算法最早由福特和福克逊于1956年提出,20世纪50年代福特(Ford)、福克逊(Fulkerson)建立的“网络流理论”,是网络应用的重要组成成分。


说得通俗点,可以把这幅图看作是自来水厂的供水路线:s点是自来水厂,有个单位的水源源不断地向外输出,但是,到了t点。即住户家里水量却是有限的。这是因为每条水管的容量有限,才导致水的流量也是有限的,且满足流量容量。回到图中,“自来水厂”s点称作源点,“住户家”t点称为汇点,“容量”即为边的边权。
你想让到你家的水尽量多,所以就衍生出了最大流的问题:即从源点有无限多的水输出,严格满足流量容量时,能够到达汇点的最大流量为多少?
为了解决这个问题,我们首先分析一下每条边的权值是如何影响这条路径的流量的:
如下图图1,显然最大流为4,因为尽管s1的容量为5,可以通过最大为5的水流,但是12的容量只有4,最多只能容纳4个单位的水流过。

如图2,尽管其它节点的容量很大,但是3t的容量只有1,所以最大流为1
所以,某条路径上的最大流量取决于整条路上容量的最小值,即一条路径{v1,v2,v3,vn}的最大流max=mini=1n(avi.dis)

求最大流的算法#

EK(Edmond—Karp)算法#

首先引入增广路的概念:

  • 增广路

增广路指从st的一条路,水流流过这条路,使得当前可以到达t的流量可以增加。

通过定义,我们显然可以看出求最大流其实就是不断寻找增广路的过程。直到找不到增广路时,到汇点的流量即为最大流。
那么,如何不断寻找增广路呢?爆搜!(其实就是不断BFS),从st进行广搜,搜索一条增广路,将这条路的流量和答案都加上min,即整条路径的最小容量。但是,为了减少变量(其实就是太懒),我们完全可以用当前路径的所有边的容量减去min代替将每条边的流量增加min即可,即得到的结果为该边还能容纳多大的水流,也就是剩下的容量。所以,BFS的时候只要不断地找所有边权均>0的边即可。等到找不到增广路的时候,我们得到的答案就是最大流了
但是,这个结论似乎有一点不对劲?
如图1,此时如果BFS,计算机很可能找到的是s12t的增广路(如图中橙色路径),并将其权值都减掉1,得到图2。此时找不到任何增广路了,于是程序结束,最后得到的最大流为1。但是,显然如果是s1ts2t两条路的话,得到的最大流应该是2。换句话说,我们目前的程序有一个问题:BFS直接找增广路并操作得到的并不一定是最大流。
针对这个问题,我们可以引入图的反向边来解决。如图3,在建图时加入反向边,且权值为0,方向与原边相反

当有反向边存在后,找到增广路后将权值减少min后,将反向边的权值加min(如图4)。然后继续BFS,反向边也算进去,就会找到如图5橙色部分所示的一条增广路s21t。再找到最小容量min,同样操作后得到图6。这时我们发现:现在的图和通过我们之前说的s1ts2t两条路之后的结果一样。所以我们可以得出反向边的用途:

也可以理解成,反向边的作用是(以上述例子解释):当有更好情况但是已经被前面搜到的增广路占用时(上述例子中为v2,t),利用反向边v2,1退给v1,2min的流量,即有min的流量不往v1,2v2,t流而改往v1,t流,因为我们搜第二条增广路时将v1,t的容量也减了min,也就是流量增加了min。这样就给想往v2,t流的流量留出了位置,于是就会有min的流量往v2,t流,而v2,t的流量相当于减了min又加了min,相当于不变。

  • 代码

P3376 【模板】网络最大流 为例

  1. 建图
    建图的方法很多,我比较喜欢采用邻接表(链式前向星)存储:
struct node{//结构体
	int to,dis,nex;
        //to:边指向的节点 dis:权值/容量 nex:同起点的下一条边的下标
}a[maxm*2];
int head[maxn],num=1;
//head[i]表示以i为起点的第一条边在a中的下标,num记录a中最大下标(总边数)
void add(int from,int to,int dis){//增加一条边
	a[++num].to=to;
	a[num].dis=dis;
	a[num].nex=head[from];
	head[from]=num;
}
  1. BFS找增广路
    从源点s开始,BFS搜每个节点,当且仅当边权(即容量)>0时进入,到达汇点t时即为一条增广路。
bool bfs(){
	queue<int> q;//队列
	memset(vis,0,sizeof(vis));
	vis[s]=1;//是否搜过
	q.push(s);
	while(!q.empty()){
		int top=q.front();//队首
		q.pop();
		for(int i=head[top];i;i=a[i].nex){//搜所有从top开始的边
			if(!vis[a[i].to]&&a[i].dis){//若没去过且容量大于0
				vis[a[i].to]=1;//标记去过
				p[a[i].to].pre=top;//记录路径,pre为上一个点
				p[a[i].to].edge=i;//记录路径,edge为边
				if(a[i].to==t){//到汇点返回1,表示有增广路
					return 1;
				}
				q.push(a[i].to);
			}
		}
	}
	return 0;//若全部搜完还没有出现增广路,返回0
}
  1. EK算法核心
    不断BFS搜增广路,更新权值。搜不到增广路时答案即为最大流。
  • 特别注意,代码中将下标异或1后得到的即为其反向边。因为建边时边和其反向边总是成对出现且反向边为奇数。
void ek(){
	long long ans=0;
	while(bfs()){//不断找增广路
		int minn=99999999;
		for(int i=t;i!=s;i=p[i].pre){
			minn=min(minn,a[p[i].edge].dis);//找最小容量
		}
		for(int i=t;i!=s;i=p[i].pre){//更改
			a[p[i].edge].dis-=minn;
			a[p[i].edge^1].dis+=minn;
		}
		ans+=minn;
	}
	printf("%lld",ans);//输出最大流
}
  1. 完整代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#define maxn 210
#define maxm 5005
using namespace std;
int n,m,s,t,u,v,w;
struct node{
	int to,dis,nex;
}a[maxm*2];
int head[maxn],num=1;
void add(int from,int to,int dis){
	a[++num].to=to;
	a[num].dis=dis;
	a[num].nex=head[from];
	head[from]=num;
}
bool vis[maxn];
struct path{
	int pre,edge;
}p[maxm*2];
bool bfs(){
	queue<int> q;
	memset(vis,0,sizeof(vis));
	vis[s]=1;
	q.push(s);
	while(!q.empty()){
		int top=q.front();
		q.pop();
		for(int i=head[top];i;i=a[i].nex){
			if(!vis[a[i].to]&&a[i].dis){
				vis[a[i].to]=1;
				p[a[i].to].pre=top;
				p[a[i].to].edge=i;
				if(a[i].to==t){
					return 1;
				}
				q.push(a[i].to);
			}
		}
	}
	return 0;
}
void ek(){
	long long ans=0;
	while(bfs()){
		int minn=99999999;
		for(int i=t;i!=s;i=p[i].pre){
			minn=min(minn,a[p[i].edge].dis);
		}
		for(int i=t;i!=s;i=p[i].pre){
			a[p[i].edge].dis-=minn;
			a[p[i].edge^1].dis+=minn;
		}
		ans+=minn;
	}
	printf("%lld",ans);
}
int main(){
	scanf("%d%d%d%d",&n,&m,&s,&t);
	for(int i=1;i<=m;i++){
		scanf("%d%d%d",&u,&v,&w);
		add(u,v,w);
		add(v,u,0);
	}
	ek();
	return 0;
}

Dinic算法#

Dinic算法(又称Dinitz算法)是一个在网络流中计算最大流的强多项式复杂度的算法,设想由以色列(前苏联)的计算机科学家Yefim (Chaim) A. Dinitz在1970年提出。

让我们优化一下EK算法。
注意到,EK中,我们是一条一条地搜增广路,那么,可不可以一次搜出所有(或多条)增广路呢?于是,Dinic算法解决了需要多路增广的问题。
Dinic算法的过程如下:

  1. BFS将图分层;
  2. DFS搜出所有增广路;
  3. 重复前两步,直到找不到增广路为止。

首先看到第一步,分层有什么用呢?实际上,分层是为了让我们找到的增广路是最短的,如图1(分的层用红色数字标在节点旁边),我们就会搜s12的增广路而不是另一条更长的路了。

分完层后,我们就进行到第2DFS搜增广路了。用DFS看似时间更长,实际上它在时间变化不大的同时还能搜到所有增广路。如图2,分完层后,用DFS搜出的两条路径如图中橙色部分。进行完后和EK一样减容量、加反向边容量,再继续分层、搜索……直到没有增广路(即所有边都到不了汇点)即可结束。具体看代码解析。

  • 代码(例题还是一样的)
  1. BFS分层
    BFS时,先将源点s的层数设为01其实也没什么关系),之后广搜,找到能更新的边就更新。
bool bfs(){
	memset(dep,0x3f,sizeof(dep));//层数
	memset(vis,0,sizeof(vis));//是否入过队
	queue<int> q;
	dep[s]=0;
	q.push(s);
	vis[s]=1;
	while(!q.empty()){
		int top=q.front();
		q.pop();
		for(int i=head[top];i;i=a[i].nex){
			if(dep[a[i].to]>dep[top]+1&&a[i].dis){//可更新的就更新
				dep[a[i].to]=dep[top]+1;
				if(!vis[a[i].to]){//没入过队的入队
					vis[a[i].to]=1;
					q.push(a[i].to);
				}
			}
		}
	}
	if(dep[t]==dep[0]){//若汇点t的层数和0号节点一样,即为初始值,意味着没有增广路了,返回false
		return 0;
	}
	return 1;
}
  1. DFS搜索
    minn表示这条路的最大容量(其实就是上面介绍EK时的min),use表示用过的容量。use的主要作用是,记录当前搜过的增广路要减去的流量,当use的值等于minn时,意味着当前的边已经达到了最大容量,此时停止搜索。
ll dfs(int x,int minn){//x为当前节点编号
	if(x==t){//到达汇点
		ans+=minn;//累加答案
		return minn;
	}
	int use=0;
	for(int i=head[x];i;i=a[i].nex){
		if(a[i].dis&&dep[a[i].to]==dep[x]+1){//满足最短增广路且还有剩余容量
			int nex=dfs(a[i].to,min(minn-use,a[i].dis));//向下搜
			if(nex>0){//若结果大于0,说明有增广路,因为若下面没有增广路了则会返回use的初值0
				use+=nex;//增加当前流量
				a[i].dis-=nex;
				a[i^1].dis+=nex;
				if(use==minn){//达到最大容量
					break;
				}
			}
		}
	}
	return use;
}
  1. Dinic算法核心
    非常简洁,因为主要部分都在两个搜索里了。
void dinic(){
	while(bfs()){//若有增广路
		dfs(s,99999999);//搜索(minn的初值大一点就好)
	}
	printf("%lld",ans);//搜完输出答案
}
  1. 完整代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#define maxn 210
#define maxm 5005
#define ll long long
using namespace std;
int n,m,s,t,u,v,w;
ll ans=0;
struct node{
	int to,dis,nex;
}a[maxm*2];
int head[maxn],num=1;
void add(int from,int to,int dis){
	a[++num].to=to;
	a[num].dis=dis;
	a[num].nex=head[from];
	head[from]=num;
}
bool vis[maxn];
int dep[maxn];
bool bfs(){
	memset(dep,0x3f,sizeof(dep));
	memset(vis,0,sizeof(vis));
	queue<int> q;
	dep[s]=0;
	q.push(s);
	vis[s]=1;
	while(!q.empty()){
		int top=q.front();
		q.pop();
		for(int i=head[top];i;i=a[i].nex){
			if(dep[a[i].to]>dep[top]+1&&a[i].dis){
				dep[a[i].to]=dep[top]+1;
				if(!vis[a[i].to]){
					vis[a[i].to]=1;
					q.push(a[i].to);
				}
			}
		}
	}
	if(dep[t]==dep[0]){
		return 0;
	}
	return 1;
}
ll dfs(int x,int minn){
	if(x==t){
		ans+=minn;
		return minn;
	}
	int use=0;
	for(int i=head[x];i;i=a[i].nex){
		if(a[i].dis&&dep[a[i].to]==dep[x]+1){
			int nex=dfs(a[i].to,min(minn-use,a[i].dis));
			if(nex>0){
				use+=nex;
				a[i].dis-=nex;
				a[i^1].dis+=nex;
				if(use==minn){
					break;
				}
			}
		}
	}
	return use;
}
void dinic(){
	while(bfs()){
		dfs(s,99999999);
	}
	printf("%lld",ans);
}
int main(){
	scanf("%d%d%d%d",&n,&m,&s,&t);
	for(int i=1;i<=m;i++){
		scanf("%d%d%d",&u,&v,&w);
		add(u,v,w);
		add(v,u,0);
	}
	dinic();
	return 0;
}

但是,洛谷上却TLE了一个点,时间达到了惊人的2.2s。此时,我们就要进行传说中的当前弧优化了。

Dinic算法+当前弧优化#

当前弧优化实际上只是增加了一个数组cur,用curi代替邻接表中的headi
原理是:当我们已经搜过一条边时,一定已经让这条边无法继续增广了,所以这条边已经没什么用了,直接用cur记录下一条有用的边,搜索时就可以省时间了。

  • 代码(最终版本)

刚才TLE的点直接减到了13ms

#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#define maxn 205
#define maxm 5005
#define ll long long
#define inf 0x3fffffff
using namespace std;
int n,m,s,t,u,v,w;
int head[maxn],tt=1;
struct node{
	int to,dis,nex;
}a[maxm*2];
void add(int from,int to,int dis){
	a[++tt].to=to;
	a[tt].dis=dis;
	a[tt].nex=head[from];
	head[from]=tt;
}
bool vis[maxn];
int dep[maxn],cur[maxn];
bool bfs(){
	for(int i=0;i<=n;i++){
		vis[i]=0;
		dep[i]=inf;
		cur[i]=head[i];
	}
	queue<int> q;
	vis[s]=1;
	q.push(s);
	dep[s]=0;
	while(!q.empty()){
		int top=q.front();
		q.pop();
		for(int i=head[top];i;i=a[i].nex){
			if(dep[top]+1<dep[a[i].to]&&a[i].dis){
				dep[a[i].to]=dep[top]+1;
				if(!vis[a[i].to]){
					vis[a[i].to]=1;
					q.push(a[i].to);
				}
			}
		}
	}
	if(dep[t]==dep[0]){
		return 0;
	}
	return 1;
}
ll ans=0;
int dfs(int x,int minn){
	if(x==t){
		ans+=minn;
		return minn;
	}
	int use=0;
	for(int i=cur[x];i;i=a[i].nex){
		cur[x]=i;
		if(dep[a[i].to]==dep[x]+1&&a[i].dis){
			int search=dfs(a[i].to,min(minn-use,a[i].dis));
			if(search>0){
				use+=search;
				a[i].dis-=search;
				a[i^1].dis+=search;
				if(use==minn){
					break;
				}
			}
		}
	}
	return use;
}
void dinic(){
	while(bfs()){
		dfs(s,inf);
	}
	printf("%lld",ans);
}
int main(){
	scanf("%d%d%d%d",&n,&m,&s,&t);
	for(int i=1;i<=m;i++){
		scanf("%d%d%d",&u,&v,&w);
		add(u,v,w);
		add(v,u,0);
	}
	dinic();
	return 0;
}

最小割#

定义#

对于一个网络流图G=(V,E),其割的定义为一种点的划分方式:将所有的点划分为ST两个集合,且T=VS,其中源点sS,汇点tT
定义割(S,T)的容量c(S,T)(或c(s,t))表示所有ST的边容量之和,即c(S,T)=uS,vTc(u,v)
最小割就是求得一个割(S,T)使得c(S,T)最小。

也就是说,一个割就是:把图的所有节点分成两部分,割的容量就是所有从源点s所在的点集到另一个点集的边的容量之和。如图为一些割及其容量。

如图绿色线为四种割,其中第三条为最小割,为9(标红部分)。
注意到,这幅图的最大流也是9。那么,最大流与最小割有什么关系呢?

最大流最小割定理#

定理内容#

  1. 如果f是网络中的一个流,c(S,T)是任意一个割,那么f等于正向割边的流量与负向割边的流量之差。
  2. f(s,t)max=c(s,t)min,即最大流等于最小割

证明#

  1. 设两点集分别为ST,定义c(A,B)表示从A指向B的边的容量和(其实就是割的容量)。则只需证明f=c(S,T)c(T,S)即可。
    显然若B1B2=,则c(A,(B1B2))=c(A,B1)+c(A,B2)c((B1B2),A)=c(B1,A)+c(B2,A)
    那么若有一个节点XS
    X为源点,则c(X,ST)c(ST,X)=f
    X不是源点,则c(X,ST)c(ST,X)=0
    因为点集S中所有点都满足上述关系式,相加得到c(S,ST)c(ST,S)=f。用①式得到:

f=c(S,ST)c(ST,S)=(c(S,S)+c(S,T))(c(S,S)+c(T,S))=c(S,T)c(T,S)

  1. 1得到,对于每一个可行的流f(s,t)和一个割(S,T),我们可以得到f(s,t)=c(S,T)c(T,S)c(S,T)
    特别地,当f为最大流时,原图中一定没有增广路,即S的出边一定满流,入边一定零流,即f(s,t)=c(S,T)
    由以上两个式子得到此时f一定最大,c一定最小。即此时f为最大流,c为最小割,最大流等于最小割。

代码#

根据最大流最小割定理,用Dinic求出最大流即为最小割。

费用流—最小费用最大流#

定义#

给定网络G=(V,E),每条边除了有容量限制c(u,v),还有一个单位流量的费用w(u,v)
当(u,v)的流量为f(u,v)时,需要花费f(u,v)w(u,v)的费用。
则该网络中总花费最小的最大流称为最小费用最大流,即在最大化(u,v)Ef(u,v)的前提下最小化(u,v)Ef(u,v)×w(u,v)

那么,在有了费用(即每条边流过要缴的水费时),应该如何让流量最大且费用最少呢?
Dinic一样,我们要分层、搜增广路并更新。那么,我们应该更改哪一步呢?
显然是分层,我们可以把BFS改成已经凉了的SPFA,这样,就可以完成了。

代码#

#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#define maxn 5005
#define maxm 50005
#define ll long long
#define inf 0x3fffffff
using namespace std;
int n,m,s,t,u,v,w,cost;
int head[maxn],tt=1;
struct node{
	int to,dis,cost,nex;
}a[maxm*2];
void add(int from,int to,int dis,int cost){
	a[++tt].to=to;a[tt].dis=dis;a[tt].cost=cost;a[tt].nex=head[from];head[from]=tt;
	a[++tt].to=from;a[tt].dis=0;a[tt].cost=-cost;a[tt].nex=head[to];head[to]=tt;
}
bool vis[maxn];
int costs[maxn];
bool spfa(){
	memset(vis,0,sizeof(vis));
	memset(costs,0x3f,sizeof(costs));
	queue<int> q;
	vis[s]=1;
	q.push(s);
	costs[s]=0;
	while(!q.empty()){
		int top=q.front();
		q.pop();
		vis[top]=0;
		for(int i=head[top];i;i=a[i].nex){
			if(costs[top]+a[i].cost<costs[a[i].to]&&a[i].dis){
				costs[a[i].to]=costs[top]+a[i].cost;
				if(!vis[a[i].to]){
					vis[a[i].to]=1;
					q.push(a[i].to);
				}
			}
		}
	}
	if(costs[t]==costs[0]){
		return 0;
	}
	return 1;
}
ll ans=0,anscost=0;
int dfs(int x,int minn){
	if(x==t){
		vis[t]=1;
		ans+=minn;
		return minn;
	}
	int use=0;
	vis[x]=1;
	for(int i=head[x];i;i=a[i].nex){
		if((!vis[a[i].to]||a[i].to==t)&&costs[a[i].to]==costs[x]+a[i].cost&&a[i].dis){
			int search=dfs(a[i].to,min(minn-use,a[i].dis));
			if(search>0){
				use+=search;
				anscost+=(a[i].cost*search);
				a[i].dis-=search;
				a[i^1].dis+=search;
				if(use==minn){
					break;
				}
			}
		}
	}
	return use;
}
void dinic(){
	while(spfa()){
		do{
			memset(vis,0,sizeof(vis));
			dfs(s,inf);
		}while(vis[t]);
	}
	printf("%lld %lld",ans,anscost);
}
int main(){
	scanf("%d%d%d%d",&n,&m,&s,&t);
	for(int i=1;i<=m;i++){
		scanf("%d%d%d%d",&u,&v,&w,&cost);
		add(u,v,w,cost);
	}
	dinic();
	return 0;
}
posted @   qzhwlzy  阅读(361)  评论(1编辑  收藏  举报
编辑推荐:
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
阅读排行:
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
· 上周热点回顾(2.17-2.23)
点击右上角即可分享
微信分享提示
主题色彩