首页 > 其他分享 >网络流学习笔记

网络流学习笔记

时间:2023-09-03 16:33:04浏览次数:45  
标签:nxt frac 增广 int 复杂度 网络 笔记 学习 dis

开个坑,是个大工程,一篇可能放不下,所以后续存在形式未知。

每周日写一个小时,大概会写很久,目前处于一个咕咕的状态。
笔者是主要从 Alex_wei 的博客中学习网络流,因此本文有很多东西来自 wls 的博客,wls tql。

1. 一些有关概念

网络是一张有向图 \(G=(V,E)\),每条边 \((u,v)\) 具有流量限制,一般用 \(c(u,v)\) 表示,分为有源汇和无源汇(一般用 \(s\) 表示源点,\(t\) 表示汇点)。

流函数 \(f\):\(f(u,v)\) 表示 \((u,v)\) 边的流量。其具有以下三个性质:

  • 流量限制:\(f(u,v)\le c(u,v)\),若 \(f(u,v)=c(u,v)\) 则称作边 \((u,v)\) 满流。
  • 斜对称:\(f(u,v)=-f(v,u)\)。
  • 流量守恒:源点汇点以外的节点不截留流量,流入流出流量相等,即 \(\forall i\not=s,t,\sum f(u,i)=f(i,v)\)。

流 \(f\) 的流量:显然,\(\sum f(s,i)=\sum f(i,t)\),这两个相等的和即为流 \(f\) 的流量。

残量网络:\(G_f(V,E_f)\),对于 \(G\) 中的每条边,其在 \(G_f\) 中的流量 \(c_f(u,v)=c(u,v)-f(u,v)\),若原边满流,即 \(c_f(u,v)=0\),这条边不存在。通俗来讲将原图中的满流边删去,其余每条边的容量减去流量即可得到残量网络。

增广路:设 \(P\) 为 \(G_f(V,E_f)\) 上 \(s\) 到 \(t\) 的一条路径。无源汇网络流不讨论增广路。

割:将 \(V\) 分成两个不交点集 \(A\),\(B\),\(s\in A,t\in B\),这种划分方式称作割。定义割的容量为 \(\sum_{u\in A}\sum_{v\in B}c(u,v)\),流量为 \(\sum_{u\in A}\sum_{v\in B}f(u,v)\)。若 \(u,v\) 所属点集不同,则称 \((u,v)\) 为割边。

2. 最大流(Maximum flow)

2.1 前置

求解过程中一般采用贪心思想,不断寻找增广路,同时每条边能流满就流满,每条增广路上的边增加 \(c_f(P)=\min\limits_{(u,v)\in P}c_f(u,v)\) 的流量。

那么看一下下面的图。
image
很明显,它满足条件,但这不是最优的方案。

为了避免这种错误的影响,我们在给一条边增加一定流量时,给其反边增加相等的容量,构成可反悔贪心,也被称作退流,可以结合下图理解一下这一点。
image
上述操作被称为一次增广。

因为操作中涉及了反边,为了方便,一般将正边与反边连续存取,通过异或操作得到反边。注意,为了保证正确性,边数初值应为 \(-1\) 或 \(1\)。

2.2 FF(ford-fulkerson)算法

复杂度 \(O(Ef)\)。其实就是暴力,找到一条增广路就增加流量,直到找不到增广路。因为几乎没有任何实际用处,笔者没有学这个算法,只是看了思想。

2.3 EK(Edmonds-Karp)算法

2.3.1 简述

复杂度 \(O(VE^2)\),利用 BFS 不断寻找最短的增广路,然后从汇点反向推回源点。

2.3.2 复杂度证明

引理:每次增广后的残量网络上 \(s\) 到各个节点的最短路不减。

考虑反证,假设 \(G_{f'}\) 上 \(s\) 到 \(x\) 的最短路 \(dis'(x)\) 小于 \(G_f\) 上 \(s\) 到 \(x\) 的最短路 \(dis(x)\),且 \(x\) 是满足该性质的距离 \(s\) 最近的点。

设 \(y\) 为 \(x\) 上一个节点,由于 \(x\) 距离最近,\(dis'(y)\ge dis(y)\) 是必然的。可以得出 \(dis'(x)=dis'(y)+1\),由 \(x\) 的性质,\(dis(x)>dis(y)+1\)。

分类讨论,若 \((y,x)\in G_F\),由 BFS 性质推出 \(dis(x)\le dis(y)+1\) ,出现矛盾;若 \((y,x)\not\in G_F\) ,又因 \((y,x)\in G_{f'}\),\((x,y)\) 必然在上一轮增广中被退流消失,可得 \(dis(y)=dis(x)+1\),出现矛盾。假设不成立,引理得证。

每轮 BFS 的复杂度显然为 \(O(E)\)

若 \((u,v)\) 满流,该边会消失,其反向边会出现,易得 \((u,v)\) 与 \((v,u)\) 一定是交替出现的。\((u,v)\) 被增广时,\(dis(v)=dis(u)+1\),\((v,u)\) 被增广时,\(dis(u)=dis(v)+1\),根据引理,\(dis(u)\) 至少增加 \(2\),其最大值为 \(V\),那么每条边满流的次数是 \(O(V)\) 级别,与边数相乘,得到轮数上界 \(O(VE)\)。

至此,EK 算法复杂度为 \(O(VE^2)\) 得证。

2.3.3 代码实现:

点击查看代码
#include<bits/stdc++.h>
#define ld long double
#define ll long long
using namespace std;
const int N=2e2+5;
const int M=5e3+5;
ll limit[M<<1],fl[N];
int n,m,s,t,head[N],from[M<<1],to[M<<1],tol,fr[N];
void add(int x,int y,int z) { 
	from[++tol]=head[x]; head[x]=tol; to[tol]=y; limit[tol]=z;
	from[++tol]=head[y]; head[y]=tol; to[tol]=x; limit[tol]=0;
}
ll maxflow() {
	ll flow=0;
	while(1) {
		queue<int> q;
		memset(fl,-1,sizeof(fl));
		fl[s]=1e18,q.push(s);
		while(!q.empty()) {
			int now=q.front(); q.pop();
			for(int i=head[now],nxt=to[i];i;i=from[i],nxt=to[i])
				if(limit[i]&&fl[nxt]==-1)
					fl[nxt]=min(limit[i],fl[now]),fr[nxt]=i,q.push(nxt);
		}//BFS增广,同时得到最小流量 fl[t]
		if(fl[t]==-1) return flow;//代表不存在增广路
		flow+=fl[t];//计算流量和
		for(int i=t;i!=s;i=to[fr[i]^1]) limit[fr[i]]-=fl[t],limit[fr[i]^1]+=fl[t];//逆推每条边流量并退流
	}
}
int main() {
	ios_base::sync_with_stdio(false);
	cin.tie(nullptr); cout.tie(nullptr);
	cin>>n>>m>>s>>t; tol=1;
	for(int i=1,x,y,z;i<=m;++i) cin>>x>>y>>z,add(x,y,z);
	cout<<maxflow()<<"\n";
	return 0;
}

2.4 dinic

2.4.1 简述

EK 算法每次只增广一条路径,这无疑会大大增加增广次数。dinic 算法对这一点进行优化,通过一次 BFS 对图进行分层,经过某层 \(u\) 的流量只能流向下一层的 \(v\),也就是说删除每个节点连向深度比它小的节点的边,得到 \(G_L\),定义为层次图。对于当前的层次图,通过 DFS 求出的阻塞流 \(f\),即使得 \(s,t\) 在残量网络上不连通的最大增广流,复杂度 \(O(V^2E)\)。

当前弧优化:每个节点可能有大量出边和入边,重复的搜索会造成极大冗余,分析算法,根据贪心和 DFS 过程,如果一条边被遍历到,在它流满或与 \(t\) 不连通前不会回溯,可以得出结论:如果一条边被增广过且被增广完全(运行到下一条边或当前流量有剩余),那么这条边在本次增广中不会被再次使用,可以直接被忽略。注意,如果该边未流满,是一定不能被略过的。同时需要知道,每次赋值到原数组是为了避免一些未被增广的边被忽略,这一点的根本原因来自建边过程,可以简单手模辅助理解。

2.4.2 复杂度证明

2.4.2.1 一般情况下复杂度上界分析

(我也不知道伪没伪,欢迎提出 hack)

即得易见平凡,仿照上例显然。留作习题答案略,读者自证不难。

首先,由于 dinic 算法包含 EK 算法,可以得知每次增广后最短路不减这条性质依然成立。

先证明单轮增广复杂度为 \(O(VE)\)。

对于每条增广路,通过跳边被遍历时,显然跳边次数不超过 \(V\)。

在一次增广过程中满流的边和被遍历到但没有满流或没有合法路径的边是两个不交的集合,同时它们的并一定是 \(G_L\) 的子集,即上界为 \(E\)。

因此,单轮增广跳边次数不会超过 \(VE\),得证。

显然,层次图的层数不可能超过 \(V\),如果 dinic 每次增广后层次图层数严格递增,则说明增广轮数为 \(O(V)\) 级别。

设 \(dis(a)\) 表示 \(a\) 到 \(s\) 的距离。在 \(E\) 中,对于任意的 \((x,y)\),有 \(dis(x)+1\ge dis(y)\) 恒成立。

对于在一次增广后,在 \(E_{f'}\) 中出现的 \((x,y)\) 有:

  1. 若 \((x,y)\) 在 \(E_f\) 中出现,则说明 \((x,y)\) 尚未流满,仍有 \(dis(x)+1\ge dis(y)\)。
  2. 若 \((x,y)\) 在 \(E_f\) 中未出现,则说明其为 \((y,x)\) 满流后退流产生的反向边,有 \(dis(x)=dis(y)+1\)。

可以得出结论:这个关系在整个求解过程中始终成立。

考虑反证。假设在某次增广后 \(dis(t)=dis'(t)\)。在这时 \(s\) 到 \(t\) 的最短路 \(P\) 满足 \(dis'(y)=dis'(x)+1\)。

可以确定其中至少有一条边 \((u,v)\) 在 \(G_L\) 中是不存在的,因为全部存在则 \(s,t\) 仍然连通,该次增广未完成。

分析 \((u,v)\) 的来源:

  1. \(dis(v)<dis(u)+1\),即这条边在新一轮分层时加入 \(G_{L'}\),那么必然有增广对这两个点造成影响,又由于 \(u,v\) 需与 \(s,t\) 连通,增广的必然是 \(v\) 前边。
    若增广 \(v\) 前边,\(dis'(u)\ge dis(u)\),\(dis'(v)=dis'(u)+1>dis(v)\),\(d(v,t)\) 不变,有

    \[dis'(t)=dis'(v)+d(v,t)>dis(v)+d(v,t) \]

    导出矛盾。
  2. \((u,v)\) 是退流产生的,那么在原图中 \(dis(u)=dis(v)+1\),应用 EK 结论,得到 \(dis'(v)\ge dis(v)+2\),有

    \[dis'(t)=dis'(v)+dis'(u)+1\ge dis'(v)+dis'(v)=dis(u)+dis(v)+3=dis(t)+2 \]

    导出矛盾。

假设不成立,因此增广轮数为 \(O(V)\) 级别。

dinic 复杂度上界为 \(O(V^2E)\) 证毕。

要注意的是,这个上界是非常宽松的,大多数情况下运行的复杂度很难达到这个上界,原因是题目主要考查建模,很难构造出能将 dinic 卡到上界附近的图。

2.4.2.2 特殊情况下的复杂度分析
2.4.2.2.1 单位容量网格

复杂度为 \(O(E\min(E^{\frac{1}{2}},V^{\frac{2}{3}}))\)。

设层次编号为 \(k\) 的层次为 \(D_k\),假设已经进行了 \(E^{\frac{1}{2}}\) 轮增广,根据性质,至少分 \(E^{\frac{1}{2}}\) 层,根据鸽巢定理,总会有一个 \(k\) 的边集 \({(u,v)|u\in D_k,v\in D_{k+1},(u,v)\in E_f}\) 满足大小小于 \(E^{\frac{1}{2}}\),显然,这是图上的一个割,\(G_f\) 也最多被增广 \(E^{\frac{1}{2}}\) 次。因此增广轮数是 \(E^{\frac{1}{2}}\) 级别的。

假设已经进行了 \(2V^{\frac{2}{3}}\) 轮增广,因为至多有半数层次点数大于 \(V^{\frac{1}{3}}\),所以总会有两个相邻层次节点数均小于 \(V^{\frac{1}{3}}\),这两个层次之间的边数最大为 \(V^{\frac{2}{3}}\),因此增广轮数是 \(V^{\frac{2}{3}}\) 级别的。

2.4.2.2.2 单位容量网络,且各店入度出度均为 1

复杂度为 \(O(EV^{\frac{1}{2}})\)。

易于得知各条增广路之间不交。

假设已经进行了 \(V^{\frac{1}{2}}\) 轮增广,新的增广路长度至少为 \(V^{\frac{1}{2}}\),根据鸽巢定理,得出最多还有 \(V^{\frac{1}{2}}\) 条增广路,即增广轮数是 \(V^{\frac{1}{2}}\) 级别的。

2.4.3 代码实现:

点击查看代码
#include<bits/stdc++.h>
#define ld long double
#define ll long long
using namespace std;
const int N=2e2+5;
const int M=5e3+5;
int n,m,s,t,head[N],cur[N],from[M<<1],to[M<<1],dis[N],tol;
ll limit[M<<1];
void add(int x,int y,int z) {
	from[++tol]=head[x]; head[x]=tol; to[tol]=y; limit[tol]=z;
	from[++tol]=head[y]; head[y]=tol; to[tol]=x; limit[tol]=0;
}
ll dfs(int now,ll res) {
	if(now==t) return res;
	ll flow=0;
	for(int i=cur[now];i&&res;i=from[i]) {
		cur[now]=i;
		int c=min(res,limit[i]),nxt=to[i];
		if(dis[nxt]==dis[now]+1&&c) {
			int tmp=dfs(nxt,c);
			flow+=tmp,res-=tmp,limit[i]-=tmp,limit[i^1]+=tmp;
		}
	}
	if(!flow) dis[now]=-1;
	return flow;
}
ll maxflow() {
	ll flow=0;
	while(1) {
		queue<int> q;
		memcpy(cur,head,sizeof(head));
		memset(dis,-1,sizeof(dis));
		dis[s]=0,q.push(s);
		while(!q.empty()) {
			int now=q.front(); q.pop();
			for(int i=head[now],nxt=to[i];i;i=from[i],nxt=to[i])
				if(dis[nxt]==-1&&limit[i])
					dis[nxt]=dis[now]+1,q.push(nxt);
		}
		if(dis[t]==-1) return flow;
		flow+=dfs(s,1e18);
	}
}
int main() {
	ios_base::sync_with_stdio(false);
	cin.tie(nullptr); cout.tie(nullptr);
	cin>>n>>m>>s>>t; tol=1;
	for(int i=1,x,y,z;i<=m;++i) cin>>x>>y>>z,add(x,y,z);
	cout<<maxflow()<<"\n";
	return 0;
}

2.5 ISAP

2.5.1 简述

dinic 算法已经称得上优秀,但是每次增广后都要重新分层,显然造成了极大的时间损耗,ISAP 针对这一点进行了优化。

考虑从汇点出发进行分层,这样进行重分层更加方便。具体来说,当对节点 \(i\) 进行增广后,遍历其当前的所有出点,将 \(d_i\) 更改为 \(min_{(u,v)/in E_{f'}}d_v+1\)。(在代码中一般采用将 \(d_i\) 自增一的操作,正确性会在下文证明。)循环终止条件为 \(d_s\geq n\)。

ISAP 中同样存在当前弧优化,并且还存在 GAP 优化,即记录层深为 \(i\) 的节点个数为 \(num_i\),若某一 \(num_i=0\),即出现断层,显然不能继续增广,可以直接终止循环。

2.5.2 正确性证明

MichaelWong共同完成。

首先说明在证明中提到的边实际情况可能是一条链。

首先假设某次增广了 \(u\),设 \(u\) 能到达节点中深度最小的为 \(v\),分讨增广前的 \(d_u\) 与 \(d_v\) 的关系。

若 \(d_u=d_v+1\),大致有下图:

若 \(x\) 被阻塞,在 DFS 中流量一定到达最大值,即 \(y\),\(z\) 两条边至少被阻塞一条。若 \(y\) 被阻塞,\(u\) 并不可达 \(v\);若 \(z\) 被阻塞,则会出现断层,\(u\) 节点无效。综上,该情况并不影响正确性。

若 \(d_u=d_v\),有下图:

若 \(z\) 被阻塞。同上一种情况。若 \(x\) 被阻塞,\(d_u\) 自增,与 \(v\) 连通。

若 \(d_u<d_v\)
,见下:

\(x\) 被阻塞后,\(d_u\) 多轮自增,最终可等于 \(d_v+1\)。

若 \(d_u>d_v+1\),根据 BFS 的性质,这种边一定是在增广 \(d_u<d_v\) 情况所出现的反边,而根据增广的定义,在增广这种情况时一定有 \(d_u=d_v+1\),可得该种类型的边并不存在。

综上所述,自增操作在各种情况中的正确性都可以保证,一般情况下,这样的复杂度较低。

2.5.3 代码实现

点击查看代码
#include<bits/stdc++.h>
#define ld long double
#define ll long long
#define inf 0x3f3f3f3f
using namespace std;
const int N=2e2+5;
const int M=5e3+5;
int n,m,s,t,head[N],from[M<<1],to[M<<1],cur[M<<1],dis[N],tol,gap[N];
ll limit[M<<1];
void add(int x,int y,int z) {
	from[++tol]=head[x]; head[x]=tol; to[tol]=y; limit[tol]=z;
	from[++tol]=head[y]; head[y]=tol; to[tol]=x; limit[tol]=0;
}
ll dfs(int now,ll res) {
	if(now==t) return res;
	ll flow=0;
	for(int &i=cur[now];i&&res;i=from[i]) {
		int c=min(res,limit[i]),nxt=to[i];
		if(dis[nxt]==dis[now]-1&&c) {
			int f=dfs(nxt,c);
			flow+=f,res-=f,limit[i]-=f,limit[i^1]+=f;
		}
		if(!res) return flow;
	}
	if(--gap[dis[now]]==0) dis[s]=n;
	++dis[now],++gap[dis[now]];
	return flow;
}
ll maxflow() {
	ll flow=0; queue<int> q;
	memset(dis,inf,sizeof(dis));
	dis[t]=0,q.push(t),gap[0]=1;
	while(!q.empty()) {
		int now=q.front(); q.pop();
		for(int i=head[now],nxt=to[i];i;i=from[i],nxt=to[i])
			if(dis[nxt]==inf&&!limit[i])
				++gap[dis[nxt]=dis[now]+1],q.push(nxt);
	}
	while(dis[s]<n) {
		memcpy(cur,head,sizeof(head));
		flow+=dfs(s,1e18);
	}
	return flow;
}
int main() {
	ios_base::sync_with_stdio(false);
	cin.tie(nullptr); cout.tie(nullptr);
	cin>>n>>m>>s>>t; tol=1;
	for(int i=1,x,y,z;i<=m;++i) cin>>x>>y>>z,add(x,y,z);
	cout<<maxflow()<<"\n";
	return 0;
}

注意,上述三种方法的本质是大致相同的,如果初始流量为一,多次进行最大流,每次初始流量加一,可以直接考虑 EK 算法。ISAP 虽然理论上相对 dinic 较优,但在分层图中,依然需要多次分层。如[CTSC1999] 家园 / 星际转移问题,从汇点出发需要遍历的节点数是远大于从源点出发的,这时使用 dinic 算法会更优一些。

参考:
网络流,二分图与图的匹配 ——Alex_Wei
最大流——OI Wiki
知乎:网络流dinic算法时间复杂度证明?——yukiyama

标签:nxt,frac,增广,int,复杂度,网络,笔记,学习,dis
From: https://www.cnblogs.com/cat-and-code/p/17484680.html

相关文章

  • Lnton羚通智能分析算法灭火器摆放识别检测算法, 使用python+yolo网络深度学习技术
    灭火器摆放识别检测算法通过python+yolo网络深度学习技术,自动对指定区域灭火器是否缺失进行识别,如果没有检测到指定区域有灭火器,立即抓拍存档进行告警。YOLO系列算法是一类典型的one-stage目标检测算法,其利用anchorbox将分类与目标定位的回归问题结合起来,从而做到了高效、灵活和......
  • KDT学习笔记
    这次稍微水了点。todo:复杂度。不知道是否存在的二进制分组优化。偏序问题一般是CDQ,常数小;或者可持久化,拿来做区间问题;万能的树套树,就是吃空间。然后就是KDT,多位偏序无脑叠,空间线性,时间……玄学。有时也有更好的方法,比如用std::bitset优化偏序,不过量有限,而且我不会。......
  • 02Java学习_注意事项和学习方法
    02_Java开发注意事项细节和学习方法注意事项.java是Java文件的拓展名。源文件的基本组成部分是类--class。Java程序的执行入口是main方法,固有的书写格式为:publicstaticvoidmain(String[]args){......}java语言严格区分大小写。Java方法由一条条语句......
  • *【学习笔记】(21) Prufer 序列
    Prufer序列Prufer序列可以将一个带标号\(n\)个节点的树用\([1,n]\)中的\(n-2\)个整数表示,即\(n\)个点的完全图的生成树与长度为\(n-2\)值域为\([1,n]\)的数列构成的双射。Prufer序列可以方便的解决一类树相关的计数问题,比如凯莱定理:\(n\)个点的完全图的生成树有......
  • 记VirtualBox+Ubuntu20.4网络配置(网络互通)
    场景原先使用桥接模式确实可以满足主机与虚拟机互通,且虚拟机可访问外网。但是不知是不是就我出现这问题——选择桥接模式,主机需要打开热点,而又由于未知原因在开热点的情况下,主机网络会有一定的影响(有时很卡)。故而,想着切换一下网络配置。工具版本VirtualBox7.0.8Ubuntu20.4......
  • Python学习第二天
    一、Python2or3?Insummary:Python2.xislegacy,Python3.xisthepresentandfutureofthelanguagePython3.0wasreleasedin2008.Thefinal2.xversion2.7releasecameoutinmid-2010,withastatementofextendedsupportforthisend-of-lifereleas......
  • 2018 ACM-ICPC 亚洲青岛区域网络赛
    A.LiveLove#include"bits/stdc++.h"usingnamespacestd;usingi64=longlong;voidsolve(){intn,m;cin>>n>>m;cout<<m<<''<<n/(n-m+1)<<'\n';}int......
  • 《C++并发编程实战》读书笔记(2):线程间共享数据
    1、使用互斥量在C++中,我们通过构造std::mutex的实例来创建互斥量,调用成员函数lock()对其加锁,调用unlock()解锁。但通常更推荐的做法是使用标准库提供的类模板std::lock_guard<>,它针对互斥量实现了RAII手法:在构造时给互斥量加锁,析构时解锁。两个类都在头文件<mutex>里声明。std::......
  • 计算机网络总结
    计算机网络笔记简书1......
  • 科普:人工智能与机器学习的关系
    大家好,我是炼数之道,一个在人工智能之路上学习和摸索的算法工程师。今天的文章在前期推文的基础上,继续用通俗的话来介绍人工智能领域的基本概念。前期文章回顾:《科普:什么是机器学习》、《科普:什么是深度学习?什么是人工智能?》 那么,人工智能和机器学习之间的关系是什么呢?下图很好......