首页 > 其他分享 >*【学习笔记】(4) 网络流

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

时间:2023-10-15 10:11:38浏览次数:36  
标签:Head 增广 int 网络 笔记 学习 edge tot dis

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

标签:Head,增广,int,网络,笔记,学习,edge,tot,dis
From: https://www.cnblogs.com/jiangchen4122/p/17417621.html

相关文章

  • 第五周学习笔记
    EXT2文件系统EXT2文件系统数据结构使用mkfs创建虚拟磁盘linux命令为mke2fs[-bblksize-Nninodes]devicenblocks具体使用例:ddif=/dev/zeroof=vdiskbs=1024count=1440mke2fsvdisk1440虚拟磁盘布局Block#0:引导块B0是引导块(BootBlock),文件系统不会使用它。它......
  • 主机字节序和网络字节序
    小端字节序和大端字节序考虑一个16位整数,它由2个字节组成。内存中存储这两个字节有两种方法:一种是将低序字节存储在起始地址,这称为小端(little-endian)字节序;另一种方法是将高序字节存储在起始地址,这称为大端(big-endian)字节序。Interx86、ARM核采用的是小端模式,PowerPC、MIPSUNI......
  • 2023-2024-1 20231410刘珈岐 《计算机基础与程序设计》第3周学习总结
    2023-2024-120231410《计算机基础与程序设计》第3周学习总结•作业信息这个作业属于哪个课程https://edu.cnblogs.com/campus/besti/2023-2024-1-CFAP这个作业要求在哪里https://www.cnblogs.com/rocedu/p/9577842.html#WEEK03这个作业的目标自学计算机科学概论第......
  • 2023-2024-1学期 20231302邱之钊 《计算机基础与程序设计》第三周学习总结
    作业信息作业属于的课程2023-2024-1-计算机基础与程序设计作业要求2023-2024-1计算机基础与程序设计第一周作业作业目标数字分类与计数法、位置计数法、进制转换、模拟数据与数字数据、压缩与解压、数字化、信息安全作业正文2023-2024-1学期20231302邱之钊《......
  • [学习笔记]强连通分量
    定义什么是强连通分量?直白地说就是在一个有向图中,有一块区域,每个点都可以互相抵达。这里用一张图来说明一下。图中的\(1,2,3\)是一个强连通分量,因为他们可以互相抵达。Tarjan算法如何求强连通分量,最有名且最常用的就是Tarjan算法。先给出如下定义:\(dfn_u\):深搜时被......
  • Javascript、axios、vue基础命令快速学习
    1.js:JavaScript基础学习JavaScript基础学习简单案例1.点击img1,则展示img1图片默认,点击img2则展示img2图片2.输入框鼠标聚焦onfocus后,显示小写toLowerCase(),失去焦点onblur后显示大写toUpperCase()3.点击全选按钮,所有复选框为被选中状态,点击反选则取消勾选状态JavaScrip......
  • 2023-2024-1 20231406 《计算机基础与程序设计》第3周学习总结
    2023-2024-120231406《计算机基础与程序设计》第3周学习总结作业信息这个作业属于哪个课程<班级的链接>(如[2023-2024-1-计算机基础与程序设计](https://edu.cnblogs.com/campus/besti/2023-2024-1-CFAP)这个作业要求在哪里<作业要求的链接>(如2023-2024-1计算机基础......
  • 学习笔记五
    第11章EXT2文件系统EXT2文件系统数据结构:EXT2文件系统使用一些关键的数据结构来组织文件和目录的存储和访问。以下是EXT2文件系统中常见的数据结构:超级块(Superblock):是文件系统的起始部分,包含关键的元数据,如文件系统的大小、块的数量、inode(索引节点)的数量等信息。块组描......
  • 2023-2024-1 20231329《计算机基础与程序设计》第3周学习总结
    作业信息这个作业属于哪个课程https://edu.cnblogs.com/campus/besti/2023-2024-1-CFAP这个作业要求在哪里https://www.cnblogs.com/rocedu/p/9577842.html#WEEK03这个作业的目标 计算机科学概论第2章,第3章并完成云班课测试《C语言程序设计》第2章并完成云班课......
  • 2023_10_14_MYSQL_DAY_05_笔记
    2023_10_14_MYSQL_DAY_05_笔记https://www.cnblogs.com/tdskee/p/16536166.html{MySQL的优化多种方法(至少15条)}#查看触发器showtriggers;#删除触发器droptrigger触发器名;#建立触发器droptriggerifexistsdept_del;createtriggerdept_delafterdeleteon......