网络流笔记
网络流
几句话
网络流的思维方式类似DP
一般来说做题流程为:
- 题意分析
- 建图
- 默写板子
其中建图是关键,类似DP中的转态表示,状态转移
概念
流网络
一张有向图,有两个特殊的点,源点 ( S ) (S) (S),汇点 ( T ) (T) (T)
边权表示该边容纳流量的最大值
G = ( V , E ) G=(V,E) G=(V,E)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XOjPGfB5-1623146274605)(C:\Users\Typedef\Pictures\截图\wll1.png)]
一般认为没有反向边
可行流(f)
满足以下条件:
- 容量限制 0 ≤ f ( u , v ) ≤ c ( u , v ) 0\leq f(u,v) \leq c(u,v) 0≤f(u,v)≤c(u,v)
- 流量守恒 流进去多少就流出来多少 ( S ) (S) (S)和 ( T ) (T) (T)除外
源点到汇点的可行流
源点流向其他点的总流量
∣ f ∣ = ∑ ( s , v ) ∈ E f ( s , v ) − ∑ ( v , s ) f ( v , s ) |f|=\sum_{(s,v)\in E}f(s,v)-\sum_{(v,s)}f(v,s) ∣f∣=∑(s,v)∈Ef(s,v)−∑(v,s)f(v,s)
流入放入减流出的
最大流(最大可行流)
可行流中的流量最大值
残留网络
这个概念是对于一条可行流而言的
包含了反向边
某条边的容量 c ′ ( u , v ) c'(u,v) c′(u,v):
- c ( u , v ) − f ( u , v ) c(u,v)-f(u,v) c(u,v)−f(u,v), ( u , v ) ∈ E (u,v)\in E (u,v)∈E
- f ( u , v ) , ( v , u ) ∈ E f(u,v) ,(v,u)\in E f(u,v),(v,u)∈E
为什么要建反向边呢?因为一条可行流满流后会影响接下的最优解
原来流网络的一条可行流 f f f加上残留网络的可行流 f ′ f' f′也是一条源网络的可行流
∣ f + f ′ ∣ = ∣ f ∣ + ∣ f ′ ∣ |f+f'|=|f|+|f'| ∣f+f′∣=∣f∣+∣f′∣
对于一条可行流,我们在它的残留网络中找到一条增广路
对于一条可行流,若它的残留网络不存在增广路,则该可行流为最大流
割
一条边,将点集 V V V分为两个子集 S , T S,T S,T
S ∪ T = V S\cup T=V S∪T=V, S ∩ T = ∅ S\cap T=\varnothing S∩T=∅
满足 s ∈ S , t ∈ T s\in S,t\in T s∈S,t∈T
点集内部不一定连通
割的容量指的是所有从 S S S指向 T T T的边的容量之和
c ( S , T ) = ∑ u ∈ S ∑ v ∈ T c(S,T)=\sum_{u\in S}\sum_{v\in T} c(S,T)=∑u∈S∑v∈T
割的流量
f ( S , T ) = ∑ u ∈ S ∑ v ∈ T f ( u , v ) − ∑ v ∈ S ∑ u ∈ T f ( u , v ) f(S,T)=\sum_{u\in S}\sum_{v\in T}f(u,v)- \sum_{v\in S}\sum_{u\in T}f(u,v) f(S,T)=∑u∈S∑v∈Tf(u,v)−∑v∈S∑u∈Tf(u,v)
常说的最小割是割的容量
注意,容量不需考虑反向边,流量需要考虑反向边
因此割的容量一定比割的流量小
最大流最小割定理
G = ( V , E ) G=(V,E) G=(V,E)
等价于:
- f f f是最大流
- G f 中 不 存 在 增 广 路 G_f中不存在增广路 Gf中不存在增广路
- 存在一个割 ( S , T ) (S,T) (S,T),使得 ∣ f ∣ = c ( S , T ) |f|=c(S,T) ∣f∣=c(S,T)
求法
最大流
EK算法
- 每次在当前残留网络中寻找一条增广路(极为简单的bfs算法)
- 更新残留网络(极为简单的将 正 向 边 容 量 − k , 反 向 边 容 量 + k 正向边容量-k,反向边容量+k 正向边容量−k,反向边容量+k)
复杂度 O ( n m 2 ) O(nm^2) O(nm2)
然而实际情况是,这个复杂度经常跑不满
因此我们实际能够拿处理的范围是 1 e 4 1e4 1e4~ 1 e 5 1e5 1e5左右
即便如此,该算法的复杂的还是有些高,因此我们更常用 d i n i c dinic dinic或者 I S A P ISAP ISAP等
H L P P HLPP HLPP什么的就算了 … \dots …我不会 … \dots …
/*************************************************************************
> File Name: EK.cpp
> Author: Typedef
> Mail: 1815979752@qq.com
> Created Time: 2021年05月02日 星期日 16时49分39秒
> Tags:
************************************************************************/
#include<bits/stdc++.h>
#define INF 0x3f3f3f3f
using namespace std;
const int N=1314,M=233333;
int n,m,s,t,idx;
int pre[M];
int e[M],h[N],ne[M],f[M];//这里用f表示容量
int q[N],d[N];//这里d表示当前路径上的容量最小值
bool st[N];
void add(int x,int y,int z){
e[idx]=y,f[idx]=z,ne[idx]=h[x],h[x]=idx++;
e[idx]=x,f[idx]=0,ne[idx]=h[y],h[y]=idx++;
}
bool bfs(){//宽搜找增广路
int hh=0,tt=0;
memset(st,0,sizeof(st));
q[0]=s,st[s]=1,d[s]=INF;
while(hh<=tt){
int u=q[hh++];
for(int i=h[u];~i;i=ne[i]){
int v=e[i];
if(!st[v]&&f[i]){
st[v]=1;
pre[v]=i;
d[v]=min(f[i],d[u]);
if(v==t) return true;
q[++tt]=v;
}
}
}
return false;
}
int EK(){
int r=0;//总流量
while(bfs()){
r+=d[t];
//这里更新残留网络
for(int i=t;i!=s;i=e[pre[i]^1])//一个小trick,通过记录的边倒着更新
f[pre[i]]-=d[t],f[pre[i]^1]+=d[t];
}
return r;
}
int main(){
scanf("%d%d%d%d",&n,&m,&s,&t);
memset(h,-1,sizeof(h));
while(m--){
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
add(a,b,c);
}
printf("%d\n",EK());
return 0;
}
dinic算法
复杂度 O ( n 2 m ) O(n^2m) O(n2m)
- b f s → bfs\to bfs→建立分层图并确定有没有增广路
- d f s → dfs\to dfs→找出所有增广路路径
当前弧优化:
- 枚举到一条点时,依次枚举它的出边找增广路
- 对于这些出边中满流的边,我们记录下来
- 当通过其他路径枚举到这个点时,我们就直接跳过满流的边
/*************************************************************************
> File Name: p3376.cpp
> Author: Typedef
> Mail: 1815979752@qq.com
> Created Time: 2021年05月05日 星期三 21时14分57秒
> Tags:
************************************************************************/
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+7,M=2e5+7,INF=1e8;
int n,m,S,T;
int e[M],ne[M],h[N],idx=0;
int f[M];//f表示当前残留网络中某条边的容量
int q[N],d[N],cur[N];//q是搜索队列,d是分层图的层数,cur是当前弧优化
void add(int a,int b,int c){
e[idx]=b,ne[idx]=h[a],f[idx]=c,h[a]=idx++;
e[idx]=a,ne[idx]=h[b],f[idx]=0,h[b]=idx++;
}
bool bfs(){
int hh=0,tt=0;
memset(d,-1,sizeof(d));
q[0]=S,d[S]=0,cur[S]=h[S];
while(hh<=tt){
int u=q[hh++];
for(int i=h[u];~i;i=ne[i]){
int v=e[i];
if(d[v]==-1&&f[i]){
d[v]=d[u]+1;
cur[v]=h[v];
if(v==T) return 1;
q[++tt]=v;
}
}
}
return 0;
}
int find(int u,int limit){
if(u==T) return limit;
int flow=0;
for(int i=cur[u];~i&&flow<limit;i=ne[i]){
cur[u]=i;
int v=e[i];
if(d[v]==d[u]+1&&f[i]){
int t=find(v,min(f[i],limit-flow));
if(!t) d[v]=-1;
f[i]-=t,f[i^1]+=t,flow+=t;
}
}
return flow;
}
int dinic(){
int r=0,flow;
while(bfs()) while(flow=find(S,INF)) r+=flow;
return r;
}
int main(){
scanf("%d%d%d%d",&n,&m,&S,&T);
memset(h,-1,sizeof(h));
while(m--){
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
add(a,b,c);
}
printf("%d\n",dinic());
return 0;
}
无源汇上下界可行流
每条边都有一个容量上限和容量下限
对于一个流网络 G G G和一条可行流 f f f
我们建立一个新的 G ′ G' G′和 f ′ f' f′,使其有源点汇点,并只有容量上界
于是就让每个边的流量减去下界,上界减去下界
然而,对于一个点入度与出度不等的情况,我们无法保证容量守恒
因此我们需要想办法在源点(或汇点)与它连一条边弥补流量
不难发现,为了达成流量守恒,该边必须满流,因此实际上 f ′ f' f′是 G ′ G' G′的最大流
源网络的流是可行流 ⟺ \Longleftrightarrow ⟺新流网络上的流是最大流(源点出边满流)
/*************************************************************************
> File Name: 2188.cpp
> Author: Typedef
> Mail: 1815979752@qq.com
> Created Time: 2021年06月08日 星期二 17时23分56秒
> Tags:
************************************************************************/
#include<bits/stdc++.h>
using namespace std;
const int N=210,M=(10200+N)*2,INF=0x3f3f3f3f;
int n,m,S,T;
int h[N],e[M],f[M],l[M],ne[M],idx;
int q[N],d[N],cur[N],A[N];
void add(int a,int b,int c,int d){
e[idx]=b,f[idx]=d-c,l[idx]=c,ne[idx]=h[a],h[a]=idx++;
e[idx]=a,f[idx]=0,ne[idx]=h[b],h[b]=idx++;
}
bool bfs(){
int hh=0,tt=0;
memset(d,-1,sizeof(d));
q[0]=S,d[S]=0,cur[S]=h[S];
while(hh<=tt){
int t=q[hh++];
for(int i=h[t];~i;i=ne[i]){
int ver=e[i];
if(d[ver]==-1&&f[i]){
d[ver]=d[t]+1;
cur[ver]=h[ver];
if(ver==T) return true;
q[++tt]=ver;
}
}
}
return false;
}
int find(int u,int limit){
if(u==T) return limit;
int flow=0;
for(int i=cur[u];~i&&flow<limit;i=ne[i]){
cur[u]=i;
int ver=e[i];
if(d[ver]==d[u]+1&&f[i]){
int t=find(ver,min(f[i],limit-flow));
if(!t) d[ver]=-1;
f[i]-=t,f[i^1]+=t,flow+=t;
}
}
return flow;
}
int dinic(){
int r=0,flow;
while(bfs()) while(flow=find(S,INF)) r+=flow;
return r;
}
int main(){
scanf("%d%d",&n,&m);
memset(h,-1,sizeof(h));
S=0,T=n+1;
for(int i=0;i<m;i++){
int a,b,c,d;
scanf("%d%d%d%d",&a,&b,&c,&d);
add(a,b,c,d);
A[a]-=c,A[b]+=c;
}
int tot=0;
for(int i=1;i<=n;i++)
if(A[i]>0) add(S,i,0,A[i]),tot+=A[i];
else if(A[i]<0) add(i,T,0,-A[i]);
if(dinic()!=tot) puts("NO");
else{
puts("YES");
for(int i=0;i<m*2;i+=2)
printf("%d\n",f[i^1]+l[i]);
}
return 0;
}