网络流初步——最大流&最小割&费用流
一、基础定义
网络 :一个网络G=(V,E)是一张有向图;
容量 :图中每条边(u,v)∈E都有一个给定的权值c(u,v)被称为容量
源/汇点:两个指定的特殊节点,分别设为S/T
流函数/流量/剩余容量 :设f(x,y)中x、y均为图上节点,且满足:
- f(x,y)<=c(x,y)
- f(x,y)=-f(y,x)
- 对于除源/汇点外的任意节点,∑f(u,x)=∑f(x,v)( (u,x),(x,v)∈E )
则称f(x,y)为网络的 流函数 。对于(x,y)∈E,f(x,y)被称为边的 流量,c(x,y)-f(x,y)被称为边的 剩余容量 。网络中由源点出发的每条路径的流量总和被称为被称为 整个网络的流量
流函数性质: 上述三条性质,分别被称为 容量限制 ,斜对称 与 流量守恒 。尤其是流量守恒定律,它告诉我们网络中除源汇点之外,所有节点都不存储流,流出总量等于流入总量,就像“流”从源点源源不断地产生,在不超过容量限制的前提下不断地流经整个网络,最终全部进入汇点。
注意:每一条边的反向边都有一个负的流量
二、最大流
1.定义: 使得某一张网络的流量最大的流函数被称为最大流,此时网络的流量被称为 最大流量
2.算法: 种类很多,下面主要介绍Edmonds_Karp增广路算法 与 Dinic算法
Edmonds_Karp增广路算法
主要思想: 若存在一条路径,其最小剩余容量不为0,则称该路径为增广路。EK算法就是通过BFS不断找到增广路,直到网络中不再有增广路。显然,此时的流函数就是最大流。
流程:
在每轮寻找增广路时,EK算法仅考虑剩余容量不为0的边,找到任意一条由S到T的路径,该条路径上的最小剩余容量(设为minf)就是网络可以增加的流量
需要注意的是,当一条边的流量增加时,根据斜对称性质,其反向边流量f(y,x)<0,此时必有f(y,x)<c(y,x)。故EK算法还需要考虑每条边的反向边。
具体实现时,可以利用“成对存储”的技巧,边权的定义可以直接规定为剩余容量。
复杂度: O(nm2 ),实际上远远达不到这个上界。
一般能处理103 ~104 级别的网络
Dinic算法
在任意时刻,网络中所有点以及剩余容量大于0的边构成的子图被称为 残量网络。
S到任意点的最短距离被称为这个节点的层次,在残量网络中,满足d[y]=d[x]+1的边(x,y)构成的子图被称为分层图。分层图显然是DAG。
Dinic算法不断重复以下步骤,直到在残量网络中S不能到达T:
- 1.在残量网络上BFS求出节点的层次,构造分层图;
- 2.在分层图上dfs寻找增广路,在回溯时实时更新剩余容量
一点补充
你可能会有这样的疑问:找到一条增广路就更新,怎么就最优了呢?难道不会出现一条增广路,使得选择这条增广路比不选择这条增广路更劣吗?
事实上,在《信息学奥赛一本通》系列的《金牌导航》中就有关于这个问题的解释。答案是:“见到一条增广路就增广”的策略不能保证每次都是最优选择,但是其中隐含的退流思想使得在算法执行完毕时得到的最终结果是最优的。
什么叫 退流思想 ?事实上,我们可以将上面两个算法视为 有反悔机制的贪心,而退流思想扮演着 “反悔机制” 的角色。在一条边(u,v)被增广后,斜对称性质保证了其反向边(v,u)一定可以被增广,而如果其反向边(v,u)也被增广,则等效于先前(u,v)增加的一部分流量被”退回去”了。于是这个算法的正确性得到了保证。如果需要,在《金牌导航》中你会找到更为详细的介绍。
代码
洛谷P3376
#include<bits/stdc++.h>
using namespace std;
#define il inline
#define re register
typedef long long LL;
il int read()
{
int s=0,w=1;char c=getchar();
while(c<'0'||c>'9'){ if(c=='-') w=-1;c=getchar();}
while(c>='0'&&c<='9'){ s=(s<<1)+(s<<3)+c-'0';c=getchar();}
return s*w;
}
il LL read_ll()
{
LL s=0,w=1;char c=getchar();
while(c<'0'||c>'9'){ if(c=='-') w=-1;c=getchar();}
while(c>='0'&&c<='9'){ s=(s<<1)+(s<<3)+c-'0';c=getchar();}
return s*w;
}
const int N=210;
const int M=1e4+10;
const LL MAX=1<<29;
int n,m,S,T;
LL maxflow;
int head[N],ver[M],nxt[M],d[N],tot;
LL quan[M];
queue<int>q;
il void kkk(int u,int v,LL w){
ver[++tot]=v;
nxt[tot]=head[u];
head[u]=tot;
quan[tot]=w;//边权的意义为“剩余容量”
}
il bool bfs()
{//在残量网络中构建分层图
memset(d,0,sizeof(d));
while(!q.empty()) q.pop();
q.push(S);d[S]=1;
while(!q.empty()){
int xx=q.front();q.pop();
for(re int i=head[xx];i;i=nxt[i]){
if(quan[i] && (!d[ver[i]])){
q.push(ver[i]);
d[ver[i]]=d[xx]+1;
if(ver[i]==T) return 1;
//如果残量网络中还能找到增广路返回1
}
}
}return 0;
}
il LL dfs_di(int x,LL flow)
{//flow表示从S到x的路径上最小剩余容量
if(x==T) return flow;//找到minf(最小剩余容量)
LL rest=flow,k=0;
//rest记录当前路径(S,x)上最小剩余容量
//k记录路径(x,T)上剩余容量的减少量
//根据流量守恒,(S,x)上的流量也要相应减少
for(re int i=head[x];i && rest;i=nxt[i]){
//注意弹出条件,只要路径上有一条边的剩余容量
//变为0,那么就直接返回
if(quan[i] && d[ver[i]]==d[x]+1){
k=dfs_di(ver[i],min(rest,quan[i]) );
//是rest 不是flow!(出过错)
if(!k) d[ver[i]]=0;
//剪枝,如果任意(x,T)都不是增广路,将x去掉
quan[i]-=k;
quan[i^1]+=k;//“成对存储”技巧
rest-=k;
}
}
return flow-rest;
}
il void dinic()
{
LL flow=0;
while(bfs())
while(flow=dfs_di(S,MAX)) maxflow+=flow;
}
int main()
{
n=read(),m=read(),S=read(),T=read();
tot=1;
for(re int i=1;i<=m;i++){
int uu=read(),vv=read();LL ww=read_ll();
kkk(uu,vv,ww),kkk(vv,uu,0LL);
}
dinic();
printf("%lld",maxflow);
}
三、最小割
最小割问题是这样一类问题:设去掉某个边一个单位的容量 的代价为1,求解代价最小的的减小边容量的方案,使得源点S不能到达汇点T。我们称这个最小代价为最小割。事实上,最小割等于最大流。
可以这样理解:在求得最大流之后,一定会有一些满流边是“瓶颈”。换句话说,如果我们将一条边的容量+1,一定会使得网络的最大流+1,这就是一个“瓶颈”。同样,我们令“瓶颈”的容量-1,则最大流一定-1,将这些制约流量增大的“瓶颈”选一部分删去,就得到了最优选择,不难理解这就是最大流。总之感性理解即可,个人认为苛求十分严格的证明是没有必要的。
最小割扩充了网络流能解决的问题,提供了一种转化问题的思路:先将问题转化成最小割模型,再利用dinic进行求解
四、费用流(最小费用最大流)
费用流解决这样一类问题:如果每条边被赋予一个权值w,使得单位流量流过需要付出w的代价,求解得到最大流的最小代价。
事实上,只需将先前在残量网络上的增广路扫描改用SPFA进行即可。由于要找最短路,用dinic实现会比较复杂,故用EK算法结合SPFA求解。
洛谷P3381 【模板】最小费用最大流
#include<bits/stdc++.h>
using namespace std;
#define il inline
#define re register
#define Min(x,y) ((x)<(y)?x:y)
typedef long long LL;
il int read(){
int s=0,w=1;char c=getchar();
while(c<'0'||c>'9'){ if(c=='-') w=-1;c=getchar();}
while(c>='0'&&c<='9'){ s=(s<<1)+(s<<3)+c-'0';c=getchar();}
return s*w;
}
const int N=5010;
const int M=1e5+10;
const int MAX=1<<30;
int n,m;
namespace Graph{
int head[N],ver[M],nxt[M],cap[M],quan[M],tot=1;
il void addedge(int u,int v,int c,int w){
ver[++tot]=v,nxt[tot]=head[u],head[u]=tot;
cap[tot]=c,quan[tot]=w;
}
il void Add(int u,int v,int c,int w){ addedge(u,v,c,w),addedge(v,u,0,-w); }
} using namespace Graph;
namespace Net{
int S,T,pre[N];
bool vis[N];
LL dis[N],incf[N],maxflow,mincost;
deque<int>q;
il void build(){
S=read(),T=read();
for(re int i=1,u,v,c,w;i<=m;i++){
u=read(),v=read(),c=read(),w=read(),Add(u,v,c,w);
}
}
il bool SPFA(){
memset(vis,0,sizeof(vis));
memset(dis,0x3f,sizeof(dis));
memset(incf,0x3f,sizeof(incf));
while(!q.empty()) q.pop_front();
q.push_back(S),dis[S]=0;
while(!q.empty()){/*在残量网络上跑SPFA*/
int x=q.front();q.pop_front();vis[x]=0;
for(re int i=head[x];i;i=nxt[i]){
int v=ver[i];if(!cap[i] || dis[v]<=dis[x]+quan[i]) continue;
dis[v]=dis[x]+quan[i];
incf[v]=Min(incf[x],cap[i]);
pre[v]=i;
if(!vis[v]){
vis[v]=1;
if(q.empty() || dis[q.front()]>=dis[v]) q.push_front(v);
else q.push_back(v);
}
}
}
return dis[T]!=dis[0];
}
il void upd(){
int x=T,i;
while(x!=S){
i=pre[x],cap[i]-=incf[T],cap[i^1]+=incf[T],x=ver[i^1];
}
maxflow+=incf[T],mincost+=incf[T]*dis[T];
}
il void work(){
build();
while(SPFA()) upd();
printf("%lld %lld",maxflow,mincost);
}
}
int main()
{
n=read(),m=read();
Net::work();
}
the end