*【学习笔记】(4) 网络流

1.算法简介

网络

一个网络 \(G = (V,E)\) 是一张有向图,图中每条有向边 \((x,y) \in E\) 都有一个给定的权值 \(c(x,y)\) ,称为边的的容量。特别的,若 \((x,y) \notin E\), 则 \(c(x,y) = 0\)。图中还有两个指定的特殊节点 \(S \in V\)\(T \in V (S \neq T)\), 分别为源点和汇点。

如果把网络想象成一个自来水管道网络,那流就是其中流动的水。每条边上的流不能超过它的容量,并且对于除了源点和汇点外的所有点(即中继点),流入的流量都等于流出的流量。

\(f(u,v)\) 定义在二元组 \((u\in V,v\in V)\) 上的实数函数且满足:

  1. 容量限制:对于每条边,流经该边的流量不得超过该边的容量,即, \(f(u,v)\leq c(u,v)\)
  2. 斜对称性:每条边的流量与其相反边的流量之和为$ 0$,即 \(f(u,v)=-f(v,u)\)
  3. 流守恒性:从源点流出的流量等于汇点流入的流量,即 \(\forall x\in V-\{s,t\},\sum\limits_{(u,x)\in E}f(u,x)=\sum\limits_{(x,v)\in E}f(x,v)\)

那么 \(f\) 称为网络 \(G\) 的流函数。对于 \((u,v)\in E\)\(f(u,v)\) 称为边的 流量, \(c(u,v)-f(u,v)\) 称为边的 剩余容量。整个网络的流量为 \(\sum\limits_{(s,v)\in E}f(s,v)\) ,即 从源点发出的所有流量之和。

一般而言也可以把网络流理解为整个图的流量。而这个流量必满足上述三个性质。

注:流函数的完整定义为
\(f(u,v)=\begin{cases}f(u,v)&(u,v)\in E\\-f(v,u) & (v,u) \in E \\0 &(u,v) \notin E, (v,u) \notin E \end{cases}\)

如下图,就是一个网络,在图中,有向边用 "\(f(x,y)/c(x,y)\)" 的形式标注流量,例如 \(f(A,B) = 4\)。值得一提的是,按照流函数的定义,图中标注的每条边的反向边都有一个负的流量,例如 \(c(A,B)=5,c(B,A)=0\)(即$(B,A)\notin E $, $ f(A,B) = 4, f(B,A) = -4$,还有 \(f(C,A) = - f(A,C) = 0\) ,这些在图中没有标出,还请留意。

2.最大流

网络流中最常见的问题就是网络最大流。假定从源点流出的流量足够多,求能够流入汇点的最大流量(即使 \(\sum\limits_{(S,v)\in E}f(S,v)\)最大)。例如对上面那张网络而言,最大流是7,图中标注应该够详细了。

Edmond-Karp算法

若一条从源点 \(S\) 到汇点 \(T\) 的路径上各条边的剩余流量都大于\(0\),则称这条路径为一条 增广路。显然可以让一股流沿着增广路从 \(S\) 流到 \(T\),使得网络的流量增大。Edmond-Karp算法的思想就是不断用 BFS 寻找增广路,直至网络上不存在增广路为止。

在每轮寻找增广路的过程中,Edmond-Karp算法只考虑图中所有 \(f(x,y) < c(x,y)\)的边,用 BFS 找到任意一条从 \(S\)\(T\) 的路径,同时计算出路径上各边的剩余流量的最小值 \(minf\),则网络的流量就可以增加 \(minf\)

但是仅仅这样可能会出错。如下图。

如果我们首先找到了 \(1\rightarrow2\rightarrow3\rightarrow4\) 这条边,那么残余网络会变成这样(残余网络,顾名思义,就是容量 - 流量):

现在已经找不到任何增广路了,最终求得最大流是1。但是,很明显,如果我们分别走\(1\rightarrow3\rightarrow4\)\(1\rightarrow2\rightarrow4\),是可以得到2的最大流的。

这时候,开始建的反向边就起作用了。


如果我们仍然选择 \(1\rightarrow2\rightarrow3\rightarrow4\),但根据反向边的定义,反向边要加上等量的容量。

这时我们可以另外找到一条增广路:\(1\rightarrow3\rightarrow2\rightarrow4\)

其实可以把反向边理解成一种撤销,走反向边就意味着撤回上次流经正向边的若干流量,这也合理解释了为什么扣除正向边容量时要给反向边加上相应的容量:反向边的容量意味着可以撤回的量,本质上就是一种反悔贪心。

加入了反向边这种反悔机制后,我们就可以保证,当找不到增广路的时候,流到汇点的流量就是最大流。

Edmond-Karp算法时间复杂度为 \(O(nm^2)\)。然而实际应用中则远远达不到这个上界,效率较高,一般能处理$10 ^ 3 \sim 10^4 $规模的网络。

关于增广有一个常用技巧:成对变换。网络流建图一般使用链式前向星,我们将每条边与它的反向边按编号连续存储,编号分别记为 \(k\)\(k+1\),其中 \(2∣k\),从而快速求得 \(k\) 的反向边编号为 \(k\) xor \(1\)。为此,初始边数 \(tot\) 应设为 \(1\),这一点千万不要忘记!

#include<cstdio>
#include<queue>
#define M 10005
#define INF 0x3f3f3f3f3f3f3f3f
#define N 205
#define LL long long
using namespace std;
int n,m,s,t,tot=1;
LL maxflow;
LL Head[N],edge[M],to[M],Next[M];
LL incf[N],pre[N];
void add(int u,int v,int w){
	to[++tot]=v,edge[tot]=w,Next[tot]=Head[u],Head[u]=tot;
	to[++tot]=u,edge[tot]=0,Next[tot]=Head[v],Head[v]=tot; //反边
}
bool bfs(){ // 寻找增广路
	int vis[N]={0};
	queue<int> q;
	q.push(s);
	vis[s]=1;
	incf[s]=INF;
	while(!q.empty()){
		int x=q.front();
		q.pop();
		for(int i=Head[x];i;i=Next[i]){
			if(!edge[i]) continue;
			int y=to[i];
			if(vis[y]) continue;
			incf[y]=min(edge[i],incf[x]);
			pre[y]=i;
			q.push(y);
			vis[y]=1;
			if(y==t) return true;
		}
	}
	return false;
}
void update(){
	int x=t;
	while(x!=s){
		int i=pre[x];
		edge[i]-=incf[t];
		edge[i^1]+=incf[t];
		x=to[i^1];
	}
	maxflow+=incf[t];
}
int main(){
	scanf("%d%d%d%d",&n,&m,&s,&t);
	for(int i=1;i<=m;i++){
		int u,v,w;
		scanf("%d%d%d",&u,&v,&w);
		add(u,v,w);
	}
	while(bfs()) update();
	printf("%lld\n",maxflow);
	return 0;
}

复杂度证明

在任意时刻,网络中所有节点以及剩余容量大于0的边构成的子图被称为残余网络。

为证明 EK 的时间复杂度,我们需要这样一条引理:每次增广后残量网络上 \(S\) 到每个节点的最短路长度 不减。

考虑反证法,假设存在节点 \(x\) 使得 \(G_{f′}\)\(S→x\) 的最短路 \(dis′_x\) 小于 \(G_f\)\(S→x\) 的最短路 \(dis_x\)(即 \(dis′_x < dis_ x\),其余点 \(z\),有 \(dis_z \le dis′_z\)),设\(G_f′\)\(S\) 到 $ x $的路径为 $ S⇝y→x \(。(\)⇝$ 表示经过若干条边)。

分类讨论 \(y→x\)

如果\((y,x) \in E_f\), 则 $dis_x \le dis_y +1 $,有 \(dis′_y +1 =dis′_x < dis_x \le dis_y +1\),得出 $dis′_y \le dis_y $,与假设不符。

如果 \((y,x) \notin E_f\) ,因为 \((y,x) \in E_f′\),所以 \((x,y)\)必然被增广了,即 $dis_x + 1= dis_y $ ,增广路是最短路。所以 \(dis′_y +1 =dis′_x < dis_x = dis_y - 1\),与 $dis_y <dis′_y $矛盾。

引理证毕。

接下来证明 EK 算法的复杂度。

不妨设某次增广的增广路为 \(P\)。根据能流满就流满的原则,存在 \((x,y)\) 使其当前剩余流量 \(c_f(x,y)\) 等于本次增广流量 \(c_f(P)\)。这使得 \((x,y)\) 属于原来的残量网络,但不在增广后的残量网络上。我们称这种边为 关键边,即剩余流量等于这次增广的流量(相当于是增广路上权值最小的边)。

对于关键边 $(x,y) $,由于 $ (x,y) $在最短路上,有 \(dis_y = dis_x +1\)
而增广后, \((x,y)\)将会从 $ G_f$中消失,重新出现的条件是 $ (y,x)$ 出现在增广路上。那么则有 $ dis′_x = dis′_y + 1$
由引理我们知道 \(dis′_y \ge dis_y\)
故有 \(dis′_x \ge dis_y + 1 = dis_x + 2\)
所以每次出现至少会使得最短距离 $ +2$,而其距离最大为 \(n - 2\),所以每条边最多做关键边 $\dfrac{n}{2}-1 $次,有 \(m\) 条边,总的增广次数就为 \(O ( nm )\)

而每次增广的 BFS 的复杂度为 \(O(m)\),所以总时间复杂度为 \(O(nm^2)\)

Dinic算法

因为 EK 算法每轮遍历整张图只会找出一条最短路,所以还有进一步的优化空间。

最常用的网络流算法是Dinic算法。作为EK算法的优化,它选择了先用BFS分层,再用DFS寻找。它的时间复杂度上界是 \(O(n^2m)\)。所谓分层,其实就是预处理出源点到每个点的距离(注意每次循环都要预处理一次,因为有些边可能容量变为0不能再走)。我们只往层数高的方向增广,可以保证不走回头路也不绕圈子。

我们可以使用多路增广节省很多花在重复路线上的时间:在某点DFS找到一条增广路后,如果还剩下多余的流量未用,继续在该点DFS尝试找到更多增广路。

此外还有当前弧优化。因为在Dinic算法中,一条边增广一次后就不会再次增广了,所以下次增广时不需要再考虑这条边。我们把head数组复制一份,但不断更新增广的起点。

#include<cstdio>
#include<queue>
#include<cstring>
#define M 10005
#define INF 0x3f3f3f3f3f3f3f3f
#define N 205
#define LL long long
using namespace std;
int n,m,s,t,tot=1;//attention!!! 好多时候我出错都是因为这里 
LL maxflow,now[N],d[N];
LL Head[N],edge[M],to[M],Next[M];
void add(int u,int v,int w){
	to[++tot]=v,edge[tot]=w,Next[tot]=Head[u],Head[u]=tot;
	to[++tot]=u,edge[tot]=0,Next[tot]=Head[v],Head[v]=tot;
}
bool bfs(){//分层 
	memset(d,0,sizeof(d));
	while(!q.empty()) q.pop();
	q.push(s),d[s]=1;now[s]=Head[s];
	while(!q.empty()){
		int x=q.front();q.pop();
		for(int i=Head[x];i;i=Next[i]){
			int y=to[i];if(d[y]||!edge[i]) continue;//容量为 0了就不要了
			now[y]=Head[y];
			d[y]=d[x]+1;
			if(y==t) return 1;
			q.push(y);
		}
	}
	return 0;
}
int dinic(int x,LL flow){
	if(x==t||!flow) return flow;
	LL rest=flow;
	for(int i=now[x];i;i=Next[i]){
		now[x]=i;//当前弧优化 
		int y=to[i];if(d[y]!=d[x]+1||!edge[i]) continue;
		int k=dinic(y,min(rest,edge[i]));
		if(!k) d[y]=0;
		rest-=k,edge[i]-=k,edge[i^1]+=k;
		if(!rest) return flow;
	}
	return flow-rest;
}
int main(){
	scanf("%d%d%d%d",&n,&m,&s,&t);
	for(int i=1;i<=m;i++){
		int u,v,w;
		scanf("%d%d%d",&u,&v,&w);
		add(u,v,w);
	}
	LL flow=0;
	while(bfs())
		while(flow=dinic(s,INF)) maxflow+=flow; 
	printf("%lld\n",maxflow);
	return 0;
}

需要注意的是,当前弧优化必须放在循环内,即每次都更新一下,防止走出去了又走回来而\(now[x]\)还没有更新导致TLE

复杂度证明

在证明 EK 的时间复杂度时,我们使用了一个引理,就是 \(S\)

到每个节点的最短路单调不减。因为 dinic 蕴含 EK,所以该引理仍然成立。

我们现在尝试证明对于 dinic 的一次增广,\(S\)\(T\) 的最短路增加。

反证法,假设存在一次增广使得 \(S\)\(T\) 的最短路没有增加。由引理,\(S\)\(T\)的最短路不变。称其为结论 1。

考察 增广后 的一条从 \(S\)\(T\) 的最短路 \(P={S=p_0→p_1→\dots→p_{k-1}→p_k=T}\),此时 \(S→p_i\) 的最短路 \(dis′(p_i)\) 等于 \(i\)\(S→T\) 的最短路 \(dis′(T)\) 等于 \(k\)

由引理,增广前 \(S\)\(p_i\) 的最短路 \(dis(p_i) \le i\) 。由结论 1,\(dis(T)=dis′(T)=k\)

若对于所有 \(i\) 均有 \(dis(p_i)=i\),则根据 dinic 的算法流程,\(P\) 在本轮增广中被增广。因此,\(P\) 必然存在一条边不在增广后的残量网络上,这与增广后 \(P\) 是一条从 \(S\)\(T\) 的最短路矛盾,因为 \(P\) 甚至不连通。

因此,存在 \(dis(p_i)<i(0<i<k)\)\(k=1\) 时可以直接导出矛盾)。又因为 \(dis(p_k)=k\),所以必然存在 \(x\)\(x+1\) 满足\(dis(p_x)+2\le dis(p_x+1)\),即 \((p_x,p_x+1)\) 不在原来的残余网络上。

又因为增广后 \((p_x,p_x+1)\) 在残余网络上,所以 \((p_x+1,p_x)\) 被增广,即 \(dis(p_x+1)+1=dis(p_x)\)。这与 \(dis(p_x+1)−2≥dis(p_x)\)

矛盾,证毕。

  • 上述证明中我们没有用到 \(T\) 的任何性质,故同理可证一轮增广从 S 到每个点的最短路增加,前提是 \(S\) 在增广前可达该点。

这样如果最短路每次最少增加 1,那么增广轮数最多为\(n -2\) 这样,我们证明了增广轮数为 \(O(n)\) 级别。接下来考虑一轮增广的复杂度。

BFS 时间 \(O(m)\)

由于高度标号加一的判断条件限制,DFS只能沿着高度递增(+1)的方向从\(S\) 推进到 \(T\),因此单次DFS推进次数最多不超过\(n - 1\) 次,时间复杂度为 \(O(n)\)

在分层图中,每次DFS增广的结果使得一条高度递增方向上的关键边\((x,y)\) 消失,此边的反向边 \((y,x)\) 增加。如前述,增广路只能沿着高度递增的方向,而分层图不变的情况下(也就是所有顶点的高度不变),反向边是沿着高度递减方向增加的。\((x,y)\) 消失后再次出现的条件是 \((y,x)\) 为此后某次增广路上的关键边。已经说过,在同一分层图内,\((y,x)\) 方向是高度递减的,顶点层号不变的情况下\((y,x)\)不可能出现在增广路上,因此一条高度递增边 \((x,y)\) 被删除后不会再次出现。

一次增广至少令一条高度递增的关键边并删除,高度递增边数量不大于总边数 \(m\) ,于是一张分层图内DFS的次数最多为 \(m\),即复杂度为 \(O(m)\)

所以一轮增广的时间复杂度为 \(O(nm+m)\)

所以 dinic 的总时间复杂度为 \(O(n^2m)\)

2.1 例题:

1. P3376 【模板】网络最大流

模板题。

#include<cstdio>
#include<queue>
#include<cstring>
#define M 10005
#define INF 0x3f3f3f3f3f3f3f3f
#define N 205
#define LL long long
using namespace std;
int n,m,s,t,tot=1;
LL maxflow,now[N],d[N];
LL Head[N],edge[M],to[M],Next[M];
void add(int u,int v,int w){
	to[++tot]=v,edge[tot]=w,Next[tot]=Head[u],Head[u]=tot;
	to[++tot]=u,edge[tot]=0,Next[tot]=Head[v],Head[v]=tot;
}
bool bfs(){
	memset(d,0,sizeof(d)); 
	queue<int> q;
	q.push(s);
	d[s]=1;
	now[s]=Head[s];
	while(!q.empty()){
		int x=q.front();
		q.pop();
		for(int i=Head[x];i;i=Next[i]){
			int y=to[i];
			if(!edge[i]||d[y]) continue;
			now[y]=Head[y];
			q.push(y);
			d[y]=d[x]+1;
			if(y==t) return true;
		}
	}
	return false;
}
LL dinic(int x,LL flow){
	if(x==t) return flow;
	LL rest=flow,k,i;
	for(i=now[x];i&&rest;i=Next[i]){
		int y=to[i];
		now[x]=i;
		if(edge[i]&&d[y]==d[x]+1){
			k=dinic(y,min(rest,edge[i]));
			if(!k) d[y]=0;
			edge[i]-=k;
			edge[i^1]+=k;
			rest-=k;
		}
	}
	return flow-rest;
}
int main(){
	scanf("%d%d%d%d",&n,&m,&s,&t);
	for(int i=1;i<=m;i++){
		int u,v,w;
		scanf("%d%d%d",&u,&v,&w);
		add(u,v,w);
	}
	LL flow=0;
	while(bfs())
		while(flow=dinic(s,INF)) maxflow+=flow; 
	printf("%lld\n",maxflow);
	return 0;
}

各种模型

1.

参考资料:《算法竞赛进阶指南》,算法学习笔记(28): 网络流
部分证明引自: 网络流,二分图与图的匹配 https://www.luogu.com.cn/blog/Link-Cut-Treeeee/qian-tan-wang-lao-liu-jian-mu-di-ji-ji-yin-qiao

posted @ 2023-10-15 10:02  Aurora-JC  阅读(21)  评论(0编辑  收藏  举报