网络流基础
基本概念
-
源点(source) s s s,汇点 t t t。
-
容量:约等于边权。不存在的边流量可视为 0 0 0。 ( u , v ) (u,v) (u,v) 的流量通常记为 c ( u , v ) c(u,v) c(u,v)(capacity)。
-
流(flow):每条边上的流不能超过它的容量,注意这个限制是所有经过路径的边共享的。除了源点和汇点,其他所有点流入的流量都等于流出的流量。通常用 f f f 表示。
-
割:把结点分成两部分 { S , T } \{S,T\} {S,T},且满足 s ∈ S , t ∈ T s\in S,t\in T s∈S,t∈T, { S , T } \{S,T\} {S,T} 是图的一个 s s s- t t t 割, s s s- t t t 割 { S , T } \{S,T\} {S,T} 的容量为 ∑ u ∈ S ∑ v ∈ T c ( u , v ) \sum\limits_{u\in S}\sum\limits_{v\in T}c(u,v) u∈S∑v∈T∑c(u,v)。
-
残留网络:有源点、汇点,且每条边都有残留容量的网络。
-
增广路:从残留网络的源点到汇点的路径。对于增广路,给每一条边都加上等量流量,此过程称为增广。
常见问题
- 最大流问题:给定每条边的流量,求得尽可能大的流量。
- 最小割问题:给定每条边的流量,求一个容量尽可能大的 s s s- t t t 割 { S , T } \{S,T\} {S,T}。
- 最小费用最大流问题:给定每条边的流量和权值(费用),求对于所有可能的最大流,费用最小的一个。
- 上下界网络流问题:给定每条边的流量上界和下界,求一种可行的流使得满足限制。
最大流问题
以下代码均为 P3376 【模板】网络最大流 代码。
先介绍一种思想——Ford–Fulkerson 增广(FF 增广)。即不断在残留网络中找一条增广路,向汇点发送可能的最大流量,得到新的残留网络,不断寻找增广路,直到没有增广路为止。此时有最大流。
在 FF 增广的过程中,为了保证正确性,我们要引入反向边。对于每一条边 ( u , v ) (u,v) (u,v),建一条 c ( v , u ) = 0 c(v,u)=0 c(v,u)=0 的反向边。反向边其实相当于一种撤回操作,因此在增广的过程中,给正向边减去流量的同时要给反向边加上流量。
反向边的“抵消”操作使得在错误的增广路选择顺序下也可以得到正确答案。
FF 增广的时间复杂度为 O ( E f max ) O(Ef_{\max}) O(Efmax)。
EK 算法
通过 BFS 实现的 FF 增广过程。最坏时间复杂度为 O ( V E 2 ) O(VE^2) O(VE2),一般可以处理 1 0 4 10^4 104 规模的网络。
注意这里的链前 cnt
初始值要设定为
1
1
1,方便通过异或操作查找反向边(这样可以使第一条边的编号为偶数,
2
n
⊕
1
=
2
n
+
1
,
(
2
n
+
1
)
⊕
1
=
2
n
2n \oplus 1=2n+1,(2n+1)\oplus 1=2n
2n⊕1=2n+1,(2n+1)⊕1=2n)。用 pre
数组记录当前的增广路,flow
数组记录当前增广路上的流量。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll maxn=5005;
int n,m,s,t,cnt=1,head[maxn],pre[maxn];
ll flow[maxn]/*记录到x的最大可行流*/,ans;
struct edge{int to,nxt;ll c;}e[maxn*2];
bool vis[maxn],flag[205][205];
void add(int x,int y,ll z){e[++cnt]={y,head[x],z},head[x]=cnt;}
bool bfs()
{
for(int i=1;i<=n;i++) vis[i]=0;
queue<int> q;
vis[s]=1,q.push(s),flow[s]=LLONG_MAX;
while(!q.empty())
{
int x=q.front();q.pop();
for(int i=head[x];i;i=e[i].nxt)
{
if(!e[i].c) continue;
if(vis[e[i].to]) continue;
flow[e[i].to]=min(flow[x],e[i].c),pre[e[i].to]=i,q.push(e[i].to),vis[e[i].to]=1;
if(e[i].to==t) return 1;
}
}
return 0;
}
void ek()
{
int x=t;
while(x!=s) e[pre[x]].c-=flow[t],e[pre[x]^1].c+=flow[t],x=e[pre[x]^1].to;
ans+=flow[t];
}
int main()
{
cin>>n>>m>>s>>t;
for(int i=1,u,v,w;i<=m;i++)
cin>>u>>v>>w,add(u,v,w),add(v,u,0);
while(bfs()) ek();
cout<<ans;
return 0;
}
Dinic 算法
先通过 BFS,把图根据结点到源点的距离分层,只按照层数递增的方向增广。注意每次增广后都要重新将图分层。
为了保证 Dinic 算法的时间复杂度正确性,我们需要引入当前弧优化。如果一条边
(
u
,
v
)
(u,v)
(u,v) 的容量已经用完,或
v
v
v 的后侧已经增广至阻塞,则
u
u
u 的流量无需流向出边
(
u
,
v
)
(u,v)
(u,v)。对于每个结点,维护它的出边中第一个需要尝试流出的出边。维护的这个指针称为当前弧。由于我们的边是顺次遍历的,所以当遍历到第
i
i
i 条边时,前面的边一定已经不能继续流,直接修改新的当前弧 now[x]=i
。
还可以用多路增广的方法优化时间复杂度。在某点找到一条增广路后,如果还有剩余流量,继续从该点寻找增广路。
DFS 过程中,对于当前结点 x x x,它可以分给后面结点最多 f max f_{\max} fmax 流量;对于当前访问的边 ( u , v ) (u,v) (u,v),分配的流量是最大流量与已经用的流量之差与边的容量取 min \min min 的结果。
最坏时间复杂度为 O ( V 2 E ) O(V^2E) O(V2E)。
#include <bits/stdc++.h>
using namespace std;
#define int long long
typedef long long ll;
const int maxn=5005;
int n,m,s,t,cnt=1,head[maxn],now[maxn];
ll flow[maxn],ans,dis[205];
struct edge{int to,nxt;ll c;}e[maxn*2];
void add(int x,int y,ll z){e[++cnt]={y,head[x],z},head[x]=cnt;}
bool bfs()
{
for(int i=1;i<=n;i++) dis[i]=LLONG_MAX;
queue<int> q;
q.push(s);dis[s]=0,now[s]=head[s];
while(!q.empty())
{
int x=q.front();q.pop();
for(int i=head[x];i;i=e[i].nxt)
if(e[i].c>0&&dis[e[i].to]==LLONG_MAX)
{
q.push(e[i].to),now[e[i].to]=head[e[i].to],dis[e[i].to]=dis[x]+1;
if(e[i].to==t) return 1;
}
}
return 0;
}
int dfs(int x,ll mxf)//mxf是能给后面点分配的最大流量
{
if(x==t) return mxf;
ll sum=0;//sum是从x点实际分配出的流量
for(int i=now[x];i;i=e[i].nxt)
{
now[x]=i;
if(e[i].c>0&&dis[e[i].to]==dis[x]+1)
{
int ff=dfs(e[i].to,min(mxf-sum,e[i].c));
e[i].c-=ff,e[i^1].c+=ff,sum+=ff;
if(ff==mxf) return ff;
}
}
return sum;
}
signed main()
{
cin>>n>>m>>s>>t;
for(int i=1,u,v,w;i<=m;i++)
cin>>u>>v>>w,add(u,v,w),add(v,u,0);
while(bfs()) ans+=dfs(s,LLONG_MAX);
cout<<ans;
return 0;
}
最小费用最大流问题
当 ( u , v ) (u,v) (u,v) 流量为 f ( u , v ) f(u,v) f(u,v) 时,花费的费用为 f ( u , v ) × w ( u , v ) f(u,v)\times w(u,v) f(u,v)×w(u,v),要求在最大化 ∑ ( u , v ) ∈ E f ( u , v ) \sum\limits_{(u,v)\in E}f(u,v) (u,v)∈E∑f(u,v) 的情况下最小化 ∑ ( u , v ) ∈ E f ( u , v ) × w ( u , v ) \sum\limits_{(u,v)\in E} f(u,v)\times w(u,v) (u,v)∈E∑f(u,v)×w(u,v),该问题即最小费用最大流问题。
SSP 算法
SSP(Successive Shortest Path)算法,思想是每次寻找费用最小的增广路进行增广,直到图上不存在增广路为止。
注意图中不能存在单位费用为负的圈。
具体实现就是把 EK/Dinic 算法中 BFS 找增广路的过程用 SPFA 代替,同时反向边的花费为负。时间复杂度 O ( V E f max ) O(VEf_{\max}) O(VEfmax)。
以下是基于 EK 算法的实现:
#include <bits/stdc++.h>
using namespace std;
const int maxn=5005,maxm=1e5+5;
struct edge{int to,nxt,c,w;}e[maxm];
int head[maxn],pre[maxn],dis[maxn],cnt=1,n,m,s,t,mxf,minc,flow[maxn];
bool vis[maxn];
void add(int x,int y,int z,int q){e[++cnt]=(edge){y,head[x],z,q},head[x]=cnt;}
bool spfa()
{
queue<int> q;
for(int i=1;i<=n;i++) dis[i]=INT_MAX,vis[i]=0;
q.push(s),dis[s]=0,flow[s]=INT_MAX,vis[s]=1,pre[t]=-1;
while(!q.empty())
{
int x=q.front();q.pop(),vis[x]=0;
for(int i=head[x];i;i=e[i].nxt)
if(e[i].c&&dis[e[i].to]>dis[x]+e[i].w)
{
dis[e[i].to]=dis[x]+e[i].w,pre[e[i].to]=i,flow[e[i].to]=min(flow[x],e[i].c);
if(!vis[e[i].to]) q.push(e[i].to),vis[e[i].to]=1;
}
}
return pre[t]!=-1;
}
void ek()
{
while(spfa())
{
int x=t;
mxf+=flow[t],minc+=flow[t]*dis[t];
while(x!=s) e[pre[x]].c-=flow[t],e[pre[x]^1].c+=flow[t],x=e[pre[x]^1].to;
// cout<<flow[t]<<' '<<dis[t]<<endl;
}
}
int main()
{
cin>>n>>m>>s>>t;
for(int i=1,u,v,w,c;i<=m;i++) cin>>u>>v>>w>>c,add(u,v,w,c),add(v,u,0,-c);
ek();
cout<<mxf<<' '<<minc;
return 0;
}