仿生 OIer 会梦见电子 NOI 吗?|

cat_and_code

园龄:2年2个月粉丝:4关注:10

网络流学习笔记

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

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

1. 一些有关概念

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

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

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

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

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

增广路:设 PGf(V,Ef)st 的一条路径。无源汇网络流不讨论增广路。

割:将 V 分成两个不交点集 ABsA,tB,这种划分方式称作割。定义割的容量为 uAvBc(u,v),流量为 uAvBf(u,v)。若 u,v 所属点集不同,则称 (u,v) 为割边。

2. 最大流(Maximum flow)

2.1 前置

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

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

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

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

2.2 FF(ford-fulkerson)算法

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

2.3 EK(Edmonds-Karp)算法

2.3.1 简述

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

2.3.2 复杂度证明

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

考虑反证,假设 Gfsx 的最短路 dis(x) 小于 Gfsx 的最短路 dis(x),且 x 是满足该性质的距离 s 最近的点。

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

分类讨论,若 (y,x)GF,由 BFS 性质推出 dis(x)dis(y)+1 ,出现矛盾;若 (y,x)GF ,又因 (y,x)Gf(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(VE2) 得证。

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,也就是说删除每个节点连向深度比它小的节点的边,得到 GL,定义为层次图。对于当前的层次图,通过 DFS 求出的阻塞流 f,即使得 s,t 在残量网络上不连通的最大增广流,复杂度 O(V2E)

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

2.4.2 复杂度证明

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

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

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

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

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

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

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

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

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

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

对于在一次增广后,在 Ef 中出现的 (x,y) 有:

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

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

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

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

分析 (u,v) 的来源:

  1. dis(v)<dis(u)+1,即这条边在新一轮分层时加入 GL,那么必然有增广对这两个点造成影响,又由于 u,v 需与 s,t 连通,增广的必然是 v 前边。
    若增广 v 前边,dis(u)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)dis(v)+2,有

    dis(t)=dis(v)+dis(u)+1dis(v)+dis(v)=dis(u)+dis(v)+3=dis(t)+2

    导出矛盾。

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

dinic 复杂度上界为 O(V2E) 证毕。

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

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

复杂度为 O(Emin(E12,V23))

设层次编号为 k 的层次为 Dk,假设已经进行了 E12 轮增广,根据性质,至少分 E12 层,根据鸽巢定理,总会有一个 k 的边集 (u,v)|uDk,vDk+1,(u,v)Ef 满足大小小于 E12,显然,这是图上的一个割,Gf 也最多被增广 E12 次。因此增广轮数是 E12 级别的。

假设已经进行了 2V23 轮增广,因为至多有半数层次点数大于 V13,所以总会有两个相邻层次节点数均小于 V13,这两个层次之间的边数最大为 V23,因此增广轮数是 V23 级别的。

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

复杂度为 O(EV12)

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

假设已经进行了 V12 轮增广,新的增广路长度至少为 V12,根据鸽巢定理,得出最多还有 V12 条增广路,即增广轮数是 V12 级别的。

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 进行增广后,遍历其当前的所有出点,将 di 更改为 min(u,v)Efdv+1。(在代码中一般采用将 di 自增一的操作,正确性会在下文证明。)循环终止条件为 dsn

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

2.5.2 正确性证明

MichaelWong共同完成。

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

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

du=dv+1,大致有下图:

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

du=dv,有下图:

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

du<dv
,见下:

x 被阻塞后,du 多轮自增,最终可等于 dv+1

du>dv+1,根据 BFS 的性质,这种边一定是在增广 du<dv 情况所出现的反边,而根据增广的定义,在增广这种情况时一定有 du=dv+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

本文作者:cat-and-code

本文链接:https://www.cnblogs.com/cat-and-code/p/17484680.html

版权声明:本作品采用署名—非商业性使用—相同方式共享 4.0 协议国际版许可协议进行许可。

posted @   cat_and_code  阅读(117)  评论(1编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起