网络流详解+题目

介绍

首先先了解网络和流

再了解一下网络流的相关定义:

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

网络流的常见问题
网络流问题中常见的有以下三种:最大流,最小割,费用流。

最大流
1、我们有一张图,要求从源点流向汇点的最大流量(可以有很多条路到达汇点),就是我们的最大流问题。

最小费用最大流
2、最小费用最大流问题是这样的:每条边都有一个费用,代表单位流量流过这条边的开销。我们要在求出最大流的同时,要求花费的费用最小。

最小割
3、割其实就是删边的意思,当然最小割就是割掉 \(X\) 条边来让 \(S\)\(T\) 两个集合(可以理解为 源点\(S\)汇点\(T\) 不连通)不互通。我们要求 \(X\) 条边加起来的流量总和最小。这就是最小割问题。

1. 求最大流

首先,明确最大流是干什么的

给定指定的一个有向图,其中有两个特殊的点源S(Sources)和汇T(Sinks),每条边有指定的容量(Capacity),求满足条件的从S到T的最大流(MaxFlow).

通俗一点,

就好比你家是汇 自来水厂是源

然后自来水厂和你家之间修了很多条水管子接在一起 水管子规格不一 有的容量大 有的容量小

然后问自来水厂开闸放水 你家收到水的最大流量是多少

如果自来水厂停水了 你家那的流量就是0 当然不是最大的流量

但是你给自来水厂交了100w美金 自来水厂拼命水管里通水 但是你家的流量也就那么多不变了 这时就达到了最大流

理解起来还好吧,也就是上文说的那样

1.1Edmond-Karp算法

1.1.1 算法解析

首先引入增广路的概念:

  • 增广路

在原图\(G\)中若一条从源点到汇点的路径上所有边的 ** 剩余容量都大于\(0\) **,这条路被称为增广路

通俗一点

在原图 \(G\) 中若一条从源点到汇点的路径上所有边的 剩余容量都大于 \(0\),这条路被称为增广路(Augmenting Path)。

通过定义,我们显然可以看出求最大流其实就是不断寻找增广路的过程。

可以用BFS也可以用DFS,只是BFS快一点

详细一点:就是用 BFS 找增广路,然后对其进行增广。你可能会问,怎么找?怎么增广?

1、找?我们就从源点一直 BFS 走来走去,碰到汇点就停,然后增广(每一条路都要增广)。我们在 BFS 的时候就注意一下流量合不合法就可以了。

2、增广?其实就是按照我们找的增广路在重新走一遍。走的时候把这条路的能够成的最大流量减一减,然后给答案加上最小流量就可以了。

再讲一下 反向边。增广的时候要注意建造反向边,原因是这条路不一定是最优的,这样子程序可以进行反悔(就相当于蓝色和橙色的线抵消了。)。假如我们对这条路进行增广了,那么其中的每一条边的正向边就减去流量,反向边的流量就加上它的流量。

1.1.2 复杂度

EK 算法的时间复杂度为 \(O(nm^2)\) (其中 \(n\) 为点数, \(m\) 为边数)。效率还有很大提升空间。

1.1.3 模板

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

#include <bits/stdc++.h> 
using namespace std;
int cnt=1;
int n,m,s,t;
int u,v,w;
bool vis[205];
int head[205];
struct o{
	int pre,edge;
}p[10005];

struct node{
	int next,to,w;
}e[10005];
void add(int x,int y,int w){
	e[++cnt].w=w;
	e[cnt].to=y;
	e[cnt].next=head[x];
	head[x]=cnt;
}

bool dfs(){
	queue<int>q;
	memset(vis,0,sizeof(vis));
	q.push(s);
	vis[s]=1;
	while (!q.empty()){
		int top=q.front();
		q.pop();
		for (int i=head[top];i;i=e[i].next){
			if (!vis[e[i].to]&&e[i].w){//若没去过且容量大于0
				vis[e[i].to]=1;//标记去过
				p[e[i].to].edge=i;//记录路径,edge为边
				p[e[i].to].pre=top;//记录路径,pre为上一个点
				if (e[i].to==t){
					return 1;
				}q.push(e[i].to);
			}
		}
	}return 0;//若全部搜完还没有出现增广路,返回0
}

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);
    }
    long long ans=0;
    while (dfs()){
    	int minn=999999999;
    	for (int i=t;i!=s;i=p[i].pre) //找最小容量
    		minn=min(minn,e[p[i].edge].w);
    	for (int i=t;i!=s;i=p[i].pre){//更改
    		e[p[i].edge].w-=minn;
    		e[p[i].edge^1].w+=minn;
		}
		ans+=minn;
    }
    printf("%lld",ans);
    return 0;
}

1.2 Dinic算法

1.2.1 算法思路

Dinic算法的思想也是分阶段地在层次网络中增广。每次增广前,我们先用 BFS 来将图分层。设源点的层数为 ,那么一个点的层数便是它离源点的最近距离。

通过分层,我们可以干两件事情:

1、 如果不存在到汇点的增广路(即汇点的层数不存在),我们即可停止增广。
2、 确保我们找到的增广路是最短的。

它与最短增广路算法不同之处是:最短增广路每个阶段执行完一次BFS增广后,要重新启动BFS从源点Vs开始寻找另一条增广路;而在Dinic算法中,只需一次DFS过程就可以实现多次增广,这是Dinic算法的巧妙之处。Dinic算法具体步骤如下:

(1)初始化容量网络和网络流。

(2)构造残留网络和层次网络,若汇点不再层次网络中,则算法结束。

(3)在层次网络中用一次DFS过程进行增广,DFS执行完毕,该阶段的增广也执行完毕。

(4)转步骤(2)。

在Dinic的算法步骤中,只有第(3)步与最短增广路相同。在下面实例中,将会发现DFS过程将会使算法的效率有非常大的提高。

构造层次网络就是给图分层,使得增广路最短

1.2.2 复杂度

因为一次DFS的复杂度为 \(O(nm)\) ,所以,Dinic算法的总复杂度即\(O(n^2m)\)

1.2.3 模板

还是以模板题为例

#include <bits/stdc++.h>
#define LL long long
using namespace std;
int cnt=1;
int n,m,s,t;
int u,v,w;
bool vis[205];
int head[205];
int d[205];
struct node{
	int next,to,w;
}e[10005];

LL ans=0;

inline int read(){   
	int x=0,f=1;  
	char ch=getchar();  
	while(ch<'0'||ch>'9'){  
		if(ch=='-')  
		f=-1;  
		ch=getchar();  
	}  
	while(ch>='0'&&ch<='9'){  
		x=x*10+ch-'0';  
		ch=getchar();  
	}  
	return x*f;  
}


void add(int x,int y,int w){
	e[++cnt].w=w;
	e[cnt].to=y;
	e[cnt].next=head[x];
	head[x]=cnt;
}

bool bfs(){
	memset(d,0x3f,sizeof(d));
	memset(vis,0,sizeof(vis));
	queue<int>q;
	vis[s]=1;
	d[s]=0;
	q.push(s);
	while (!q.empty()){
		int top=q.front();
		q.pop();
		for (int i=head[top];i;i=e[i].next){
			if (d[e[i].to]>d[top]+1&&e[i].w){
				d[e[i].to]=d[top]+1;
				if (!vis[e[i].to]){
					vis[e[i].to]=1;
					q.push(e[i].to);
				}
			}
		}
	}
	if (d[t]==d[0]){
		return 0;
	}
	return 1;
}
LL dfs(int x,int minn){
	int use=0;
	if (x==t){
		ans+=minn;
		return minn;
	}
	for (int i=head[x];i;i=e[i].next){
		if (e[i].w&&d[e[i].to]==d[x]+1){
			int nex=dfs(e[i].to,min(minn-use,e[i].w));
			if (nex>0){
				use+=nex;
				e[i].w-=nex;
				e[i^1].w+=nex;
				if (use==minn)
					break;
			}
		}
	}
	return use;
}


int main(){
	n=read();m=read();s=read();t=read();
    for (int i=1;i<=m;i++){
    	u=read();v=read();w=read();
        add(u,v,w);
        add(v,u,0);
    }
    while (bfs()){
    	dfs(s,999999999);
    }
    printf("%lld",ans);
    return 0;
}

然后我们就会发现,我们TLE了一个点,这是就需要优化,可以想到至高无上的弧优化。

1.3 Dinic算法+当前弧优化

1.3.1 算法优化

当前弧优化实际上只是增加了一个数组 \(cur\),用 \(cur_i\) 代替邻接表中的 \(head_i\)
原理是:当我们在dfs时,从u点出发访问到了第i条边,就说明前i-1条边都已经被榨干了,所以才到了第i条边,因此我们直接改cur[u] = i,下次访问从u点出发的边时,就会直接访问第i条边,省略了前面i-1条边。

不懂的话看看下面代码模板,就会理解了(或者可以去了解一下链式前向星)。

1.3.2 完整代码

#include <bits/stdc++.h>
#define LL long long
using namespace std;
int cnt=1;
int n,m,s,t;
int u,v,w;
bool vis[205];
int head[205];
int d[205];
int cur[205];
struct node{
	int next,to,w;
}e[10005];

LL ans=0;

inline int read(){   
	int x=0,f=1;  
	char ch=getchar();  
	while(ch<'0'||ch>'9'){  
		if(ch=='-')  
		f=-1;  
		ch=getchar();  
	}  
	while(ch>='0'&&ch<='9'){  
		x=x*10+ch-'0';  
		ch=getchar();  
	}  
	return x*f;  
}


void add(int x,int y,int w){
	e[++cnt].w=w;
	e[cnt].to=y;
	e[cnt].next=head[x];
	head[x]=cnt;
}

bool bfs(){
	for (int i=0;i<=n;i++){
		d[i]=0x3ffffff;
		vis[i]=0;
		cur[i]=head[i];
	}
	queue<int>q;
	vis[s]=1;
	d[s]=0;
	q.push(s);
	while (!q.empty()){
		int top=q.front();
		q.pop();
		for (int i=head[top];i;i=e[i].next){
			if (d[e[i].to]>d[top]+1&&e[i].w){
				d[e[i].to]=d[top]+1;
				if (!vis[e[i].to]){
					vis[e[i].to]=1;
					q.push(e[i].to);
				}
			}
		}
	}
	if (d[t]==d[0]){
		return 0;
	}
	return 1;
}
LL dfs(int x,int minn){
	int use=0;
	if (x==t){
		ans+=minn;
		return minn;
	}
	for (int i=head[x];i;i=e[i].next){
		cur[x]=i;
		if (e[i].w&&d[e[i].to]==d[x]+1){
			int nex=dfs(e[i].to,min(minn-use,e[i].w));
			if (nex>0){
				use+=nex;
				e[i].w-=nex;
				e[i^1].w+=nex;
				if (use==minn)
					break;
			}
		}
	}
	return use;
}


int main(){
	n=read();m=read();s=read();t=read();
    for (int i=1;i<=m;i++){
    	u=read();v=read();w=read();
        add(u,v,w);
        add(v,u,0);
    }
    while (bfs()){
    	dfs(s,0x3ffffff);
    }
    printf("%lld",ans);
    return 0;
}

2.求最小割

\((CUT)\)
割是网络中顶点的划分,它把对于一个网络流图 \(G=(V,E)\)的所有顶点划分成两个集合 \(S\)\(T\),且\(S+T=V\) ,其中源点\(s∈S\),汇点\(t∈T\)。记为\(c(S,T)\)

定义割\((S,T)\)的容量\(c(S,T)\)表示所有从\(S\)\(T\)的边的容量之和(注意是有方向的),即\(c(S,T)=\sum\limits_{u\in S, v \in T}c(u,v)\)

就是一个恐怖分子想要,砍断水管使你家断水,至少要砍断那些水管使水管容量和最小

显然上图的最小割是8,但是一想,最大流也是8,这之间有什么关系吗?

2.1 最大流最小割定理

在任何的网络中,最大流的值等于最小割的容量

具体的证明分三部分

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

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

相当于在残量网络中,源点能到达的结点的各个边的容量和为最大流

所以如果我们要求一个最小割的边集,我们只要跑一编最大流,然后在残量网络中找正向边残量为0的边,那么这条边肯定在最小割里面,
这样就可以得到一组最小割的边集

3.最大流等于最小割
设相等的流和割分别为F_m和C_m
则因为任意一个流小于等于任意一个割

  任意F≤F_m=C_m≤任意C 

费用流

介绍

给定一个网络 \(G=(V,E)\),每条边除了有容量限制 \(w(u,v)\) 还有一个单位流量的费用 \(cost(u,v)\)
也就是说在\((u,v)\) 流量为 \(f(u,v)\) 时,需要 \(f(u,v)*cost(u,v)\) 的费用

则该网络中总花费最小的最大流称为 最小费用最大流,即在最大化\(\sum_{(u,v)\in E}f(u,v)\)的前提下最小化\(\sum_{(u,v)\in E}f(u,v)*cost(u,v)\)

SSP算法

SSP(Successive Shortest Path)算法是一个贪心的算法。它的思路是每次寻找单位费用最小的增广路进行增广,直到图上不存在增广路为止。

如果图上存在单位费用为负的圈,SSP 算法正确无法求出该网络的最小费用最大流。此时需要先使用消圈算法消去图上的负圈。(一般用不上)

实现

只需将EK 算法 Dinic 算法中找增广路的过程,替换为用最短路算法寻找单位费用最小的增广路即可。

以模板题P3381 【模板】最小费用最大流为例

#include<bits/stdc++.h>
#define ll long long
#define inf 0x3fffffff
using namespace std;
int u,v,w,c,s,t,n,m;
int head[5005];
bool vis[5005];int cost[5005];
struct node{
	int next,to;
	int cost,w;
}e[100010];int cnt=1;
void add(int x,int y,int w,int cost){
	e[++cnt].cost=cost;
	e[cnt].next=head[x];
	e[cnt].to=y;
	e[cnt].w=w;
	head[x]=cnt;
}ll ans=0,anscost=0;

int spfa(){
	memset(cost,0x3f,sizeof(cost));
	memset(vis,0,sizeof(vis));
	queue<int> q;
	vis[s]=1;
	q.push(s);
	cost[s]=0;
	while (!q.empty()){
		int top=q.front();q.pop();
		vis[top]=0;
		for (int i=head[top];i;i=e[i].next){
			if (cost[top]+e[i].cost<cost[e[i].to]&&e[i].w){
				cost[e[i].to]=cost[top]+e[i].cost;
				if (!vis[e[i].to]){
					vis[e[i].to]=1;
					q.push(e[i].to);
				}
			}
		}
	}
	if (cost[t]==cost[0])	return 0;//cost[0]是一个没有的变量,可以理解为0x3f
	else return 1;
}

int dfs(int x,int minn){
	int use=0;vis[x]=1;
	if (x==t)
		return minn;
	for (int i=head[x];i;i=e[i].next){
		if ((!vis[e[i].to]||e[i].to==t)&&cost[e[i].to]==cost[x]+e[i].cost&&e[i].w){//如果没访问过或者是终点
//且(u的花费+这条路的花费)==v的花费,且这条路的惨量大于0
			int nex=dfs(e[i].to,min(minn-use,e[i].w));
			if (nex>0){
				use+=nex;
				anscost+=(nex*e[i].cost);
				e[i].w-=nex;
				e[i^1].w+=nex;
				if (use == minn)break;
			}
		}
	}
	return use;
}

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,&c);
		add(u,v,w,c);
		add(v,u,0,-c);
	}
	while (spfa()){
		do{
			memset(vis,0,sizeof(vis));
			ans+=dfs(s,inf);
		}while (vis[t]);
	}
	printf("%lld %lld\n",ans,anscost);
	return 0;
}

后记

之后会出网络流23题(24题 - 1黑题)
让我们敬请期待

posted @ 2021-08-27 07:00  hewt  阅读(444)  评论(0编辑  收藏  举报