最大流问题(Maximum-Flow Problem)
给定带权连通图
G
=
(
V
,
E
)
G=(V,E)
G=(V,E),求源点
s
s
s 到汇点
t
t
t 的最大流。形象的描述:把上图看作一个输油管道网 ,
s
s
s 表示发送点,
t
t
t 表示接收点,其他点表示中转站 ,各边的权数表示该段管道的最大输送量,求从
s
s
s 到
t
t
t 的最大输油量。
一条边
(
u
,
v
)
(u,v)
(u,v)的最大输送量称为容量(capacity),记为
c
(
u
,
v
)
c(u,v)
c(u,v),边上实际输送的量称为流量(flow),记为
f
(
u
,
v
)
f(u,v)
f(u,v)。
最大流问题中容量
c
c
c 与流量
f
f
f 满足
3
3
3个性质:
1.容量限制:
f
(
u
,
v
)
f(u,v)
f(u,v)
≤
\le
≤
c
(
u
,
v
)
c(u,v)
c(u,v)
2.斜对称性:
f
(
u
,
v
)
=
−
f
(
v
,
u
)
f(u,v)= - f(v,u)
f(u,v)=−f(v,u)
“从
u
u
u 到
v
v
v 输送
5
5
5 单位油又从
v
v
v 到
u
u
u 输送
3
3
3 单位油” 没有什么意义,它相当于从
u
u
u 到
v
v
v 输送
2
2
2 单位油。规定负流
−
f
(
u
,
v
)
-f(u,v)
−f(u,v) 表示 从
v
v
v 到
u
u
u 输送
f
(
u
,
v
)
f(u,v)
f(u,v)单位油,即“从
u
u
u 到
v
v
v 输送
2
2
2 单位油”等价于“从
v
v
v 到
u
u
u 输送
−
2
-2
−2 单位油”。
3.流量平衡:除源点
s
s
s 和汇点
t
t
t 外的任意结点
u
u
u 满足
∑
(
u
,
v
)
∈
E
f
(
u
,
v
)
=
0
\sum_{(u,v)\in E}f(u,v)=0\quad
∑(u,v)∈Ef(u,v)=0
除源点
s
s
s 和汇点
t
t
t 任意结点流入量与流出量相等
EK 算法(Edmonds-Karp 算法)
算法思想:从零流开始不断增加可行流
可行流: 增加流量后满足容量限制、斜对称性和流量平衡3个条件
每一条边上的容量与流量之差称为残余容量,简称残量。当残量大于0时当前边上才可以增加流量。
计算出每一条边上的残量得到的图称为残量网络(residual network)。
残量网络中的边包含两种:正向边(正向弧,输入的边)和反向边(反向弧,输入的边反过来,容量为 0 0 0)。当正向弧的流量增加 f f f、残量减少 f f f,同时对应的反向弧的流量增加 − f -f −f(减少 f f f)、残量减少 − f -f −f(增加 f f f)。这样做的目的是为了给算法一个反悔的机会,反向弧增加流量就相当于正向弧减少流量,而算法本身是一直在增加可行流。
增广路径(augmenting path):残量网络中任何一条从
s
s
s 到
t
t
t 的可行路径,路径中所有残量的最小值
f
f
f大于
0
0
0。
把路径上的所有边的流量增加
f
f
f(对应反向边流量减少
f
f
f),这个过程称为增广。
找出残量网络中的增广路,进行增广,直到残量网络中不存在增广路时得到的网络流为最大网络流。这就是 Edmonds-Karp 算法 。
E
K
EK
EK算法步骤:
1.构建残量网络
2.找出增广路进行增广(BFS or DFS)
3.无增广路时结束
D i n i c Dinic Dinic 算法
E d m o n d s − K a r p Edmonds-Karp Edmonds−Karp 算法 比较简单,足以应对规模小数据不刁钻的网络流问题,但效率上有很大的提升空间。相对的 D i n i c Dinic Dinic 算法 概率也不难,而且速度不错。
D
i
n
i
c
Dinic
Dinic 算法:不停地用
B
F
S
BFS
BFS 构建层次图,然后用阻塞流来增广。
层次图: 残量网络图中每一个结点
u
u
u 到源点
s
s
s 的距离
d
i
s
(
u
)
dis(u)
dis(u) 即为点
u
u
u 所在的 ‘层次’,
B
F
S
BFS
BFS 在求
d
i
s
dis
dis 时,残量网络中残量小于等于
0
0
0 的边视为不存在,残量大于
0
0
0 的边长度(距离)视为
1
1
1。
残量网络去掉与
d
i
s
dis
dis 增加方向相反的边以及残量不大于
0
0
0的边,得到层次图。
阻塞流: 不考虑反向弧的 “最大流” ,在层次图上沿着
d
i
s
dis
dis 增大的方向进行增广,直到层次图上不存在
s
−
t
s-t
s−t 增广路径时总共增加的流称为阻塞流。
三次增广后层次图中不存在
s
−
t
s-t
s−t 增广路径,这三次增广的流合称阻塞流。
算法步骤:
1.构建残量网络
2.构建层次图
3.阻塞流增广
4.无法构建层次图(残量网络上无
s
−
t
s-t
s−t 增广路)时结束
【模板】 题:luogu P3376 模板-网络最大流
#include <iostream>
#include <queue>
#define _for(i,j,k) for(int i=j;i<=k;i++)
#define for_(i,j,k) for(int i=j;i>=k;i--)
using namespace std;
//flow: Dinic algorithm
const int maxn = 1e4+5;
const int maxm = 2e5+5;
const int inf = 1e9; //无限大
struct Edge{ //邻接表边结点
int v,cap,flow; //cap:容量;flow:流量;(可以只存残量)
};
Edge e[maxm];
int n,m,s,t,an;
int head[maxn],net[maxm],cnt=1; //邻接表
int vis[maxn],cur[maxn];
void add_edge(int u,int v,int cap,int flow){ //邻接表 存残量网络
e[++cnt].v=v;
e[cnt].cap=cap;
e[cnt].flow=flow;
net[cnt]=head[u];
head[u]=cnt;
}
bool bfs(int u){ //bfs构建层次图
memset(vis,0,sizeof(vis));
queue<int> q;
q.push(u);
vis[u]=1;
while(!q.empty()){
u=q.front();
q.pop();
for(int i=head[u];i;i=net[i]){
if(!vis[e[i].v]&&e[i].cap>e[i].flow){ //残量为正时
vis[e[i].v]=vis[u]+1;
q.push(e[i].v);
}
}
}
return vis[t];
}
int dfs(int u,int ca){ //dfs进行阻塞流增广; ca:到点u为止目前最小残量值
if(u==t||ca==0) return ca; //u为汇点或ca==0终止
int flow=0,f;
for(int& i=cur[u];i;i=net[i]){ //不加当前边优化的话:for(int i=head[u];i;i=net[i])
if(vis[u]+1==vis[e[i].v]&&(f=dfs(e[i].v,min(ca,e[i].cap-e[i].flow)))>0){
e[i].flow+=f; //更新正向流
e[i^1].flow-=f; //更新反向流 编号i和i^1的边互为反向边
flow+=f;
ca-=f;
}
if(ca==0) break;
}
return flow;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>m>>s>>t;
int u,v,cap;
_for(i,1,m){ //构建残量网络
cin>>u>>v>>cap;
add_edge(u,v,cap,0); //正向弧
add_edge(v,u,0,0); //反向弧
}
while(bfs(s)){ //构建层次图
_for(i,1,n) cur[i]=head[i]; //当前边的优化(可以不加)
an+=dfs(s,inf); //阻塞流增广
}
cout<<an;
return 0;
}
最小费用最大流
在网络流问题上给每一条边增加一个因素:单位流量所需费用(cost),用
a
(
u
,
v
)
a(u,v)
a(u,v) 表示,求花费最小的最大流。
在网络流最大的前提下求最小费用,而最大流是一定的,唯一有变化的只有在增广路的选择下,而影响费用的会是一条增广路上所有边单位流费用的和,因为一条增广路上每条边上的增广流是相等的。最终的最大流是确定的,所以每次增广时选择增广路上所有边单位流费用和最小的路径增广,直到不存在
s
−
t
s-t
s−t 增广路时结束。算法思想类似与
E
K
EK
EK算法。
将单位流费用类比距离,用
B
e
l
l
m
a
n
−
F
o
r
d
Bellman-Ford
Bellman−Ford 算法找费用和最小的增广路。
【模板】 题:luogu P3381 模板-最小费用最大流
#include <iostream>
#include <queue>
#define LL long long
#define _for(i,j,k) for(int i=j;i<=k;i++)
#define for_(i,j,k) for(int i=j;i>=k;i--)
using namespace std;
const int maxn = 5e3+5;
const int maxm = 5e5+5;
const int inf = 0x3f3f3f3f; //无限大
//min-cost-max-flow
struct Edge{
int u,v,cap,flow,cost;
};
Edge e[maxm];
int n,m,s,t;
int cnt=1,head[maxn],net[maxm]; //邻接表
int dis[maxn],pre[maxn],vis[maxn],allow[maxn]; //bf
void add_edge(int u,int v,int cap,int flow,int cost){
e[++cnt].v=v;
e[cnt].u=u;
e[cnt].cap=cap;
e[cnt].flow=flow;
e[cnt].cost=cost;
net[cnt]=head[u];
head[u]=cnt;
}
bool bf(int& flow,int& cost){
memset(dis,inf,sizeof(dis));
memset(vis,0,sizeof(vis));
dis[s]=0;
allow[s]=inf;
pre[s]=0;
queue<int> q;
q.push(s);
vis[s]=1;
int u;
while(!q.empty()){ //bellman-ford
u=q.front();
q.pop();
vis[u]=0;
for(int i=head[u];i;i=net[i]){
if(e[i].cap>e[i].flow&&dis[e[i].v]>dis[u]+e[i].cost){
dis[e[i].v]=dis[u]+e[i].cost;
pre[e[i].v]=i;
allow[e[i].v]=min(allow[u],e[i].cap-e[i].flow);
if(!vis[e[i].v]){
q.push(e[i].v);
vis[e[i].v]=1;
}
}
}
}
if(dis[t]==inf) return false;
flow+=allow[t];
cost+=allow[t]*dis[t];
u=t;
while(u!=s){
e[pre[u]].flow+=allow[t];
e[pre[u]^1].flow-=allow[t];
u=e[pre[u]].u;
}
return true;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>m>>s>>t;
int u,v,cap,flow,cost;
_for(i,1,m){
cin>>u>>v>>cap>>cost;
add_edge(u,v,cap,0,cost);
add_edge(v,u,0,0,-cost); //反向弧费用为正向弧费用的相反数
}
flow=cost=0;
while(bf(flow,cost));
cout<<flow<<" "<<cost;
return 0;
}