[学习笔记]网络流/费用流总结
这几天集中学习了一下有关网络流/费用流的问题。
自己之前也没有较为深入地研究过,只是打了几道板题然后就咕咕了。
[补]这篇博客本来该一周之前就发布的,结果忘掉了。
模板
网络流问题
level 1 EK 算法
主要算法:多次BFS(增广)
EK 算法全名Edmonds-Karp算法。
是网络流当中最基础的算法。[当然,也是均摊下来跑得最慢的一种算法]
主要用到了残余网络的一些性质。
每次找到一条可增广的最短(这里指边数最小)的路径,对其增广,直到不能增广为止。
这里不详细讲解.
复杂度上限为 。
EK算法算是最通用的一种算法,只需将BFS改为SPFA就可以做费用流了。
复杂度不变。
[不建议使用该算法]
level 2 Dinic算法
i.简介:
主要算法:DFS+分层图
主要优化了EK算法中每次只能找一条增广路径的劣势。
dinic算法在EK算法的基础上增加了分层图的概念,我们将其按离源点的最短距离的大小进行分层。
显然,我们要找的增广路上的点一定属于不同的层。
这样我们就可在一次增广中找到多条路径了。
ii.复杂度:
在普通情况下,Dinic时间复杂度为
在二分图中,Dinic算法时间复杂度为
[具体请参考其他资料]
iii.优化:
Dinic算法最主要优化的就是当前弧优化。
当前弧优化:每一次增广时不从第一条边开始,而是用一个数组记录点之前循环到了哪一条边,以此来加速
[强烈建议加上这个优化]
iv.板子:
#define INF INT_MAX
#define CLR(a,x) memset(a,x,sizeof(a))
bool bfs()//分层
{
CLR(dep,0);CLR(cur,-1);
dep[S]=1;q.push(S);
while(!q.empty()){
int p=q.front();q.pop();
for(int i=head[p];i;i=e[i].nxt){
int b=e[i].to;
if(dep[b]||!e[i].cap) continue;
dep[b]=dep[p]+1;
q.push(b);
}
}
return dep[T];
}
int dinic(int x,int y){//找多条增广路径
if(x==T) return y;
int tmp=y;
if(cur[x]==-1) cur[x]=head[x];//当前弧优化
for(int &i=cur[x];i;i=e[i].nxt)
{
int b=e[i].to;
if(dep[b]!=dep[x]+1||!e[i].cap) continue;
int re=dinic(b,min(e[i].cap,tmp));
e[i].cap-=re,e[i^1].cap+=re;
tmp-=re;
if(!tmp) break;
}return y-tmp;
}
int mian()
{
.......
while(bfs())flow+=dinic(S,INF);
.......
}
v.用途:
在竞赛中,一般用当前弧优化的Dinic算法就够了,仅有少数题目会卡掉这个算法。
level 3 ISAP算法
[作者太弱了,还没学过]
听说加了当前弧和GAP效率就挺高的了。
费用流问题
level 1
EK 算法+SPFA
[同上level1],将BFS改为SPFA即可
#define MAXN 1000000
#define MAXM 1000000
#define MAXP 300
#define MAXK 10
#define INF 0x3f3f3f3f
struct E
{
int to,cap,cost,flow,next;
}e[2*MAXM];int head[MAXN+5] , ecnt;
int pre[MAXN+5];
int dis[MAXN+5];
bool vis[MAXN+5];
int L[MAXP+5][MAXK+5],R[MAXP+5][MAXK+5];
int n,m,S,T,cnt;
void Clear()
{
ecnt = 0;
memset(head,-1,sizeof head);
}
void add(int fr,int to,int cap,int cost)
{
e[ecnt]=(E){to,cap,cost,0,head[fr]};
head[fr] = ecnt++;
e[ecnt]=(E){fr,0,-cost,0,head[to]};
head[to] = ecnt++;
}
bool SPFA(int s,int t)
{
memset(vis,0,sizeof vis);
memset(dis,0x3f,sizeof dis);
memset(pre,-1,sizeof pre);
queue <int> q;
q.push(s);dis[s] = 0;vis[s]=1;
while (!q.empty())
{
int cur = q.front();q.pop();vis[cur] = false;
for (int j=head[cur];j!=-1;j=e[j].next)
{
int to = e[j].to;
if (dis[to] > dis[cur] + e[j].cost && e[j].cap > e[j].flow )
{
dis[to] = dis[cur] + e[j].cost;
pre[to] = j;
if (!vis[to])
{
q.push(to);
vis[to] = true;
}
}
}
}
return pre[t] != -1;
}
void MCMF(int s,int t,int &maxflow,int &mincost)
{
maxflow = mincost = 0;
while (SPFA(s,t))
{
int MIN = INF;
for (int j=pre[t]; j!=-1;j=pre[e[j^1].to])
MIN = min(MIN,e[j].cap - e[j].flow);
for (int j=pre[t]; j!=-1;j=pre[e[j^1].to])
{
e[j].flow += MIN;
e[j^1].flow -= MIN;
mincost += MIN * e[j].cost;
}
maxflow += MIN;
}
}
EK算法+Dijkstra
然而…
众所周知:SPFA它死了
事实证明在任何优化下,都能够被毒瘤的出题人卡超时。
[关于SPFA的优化]
然而,费用流是有负边[这里指反边]的。堆优化dijkstra并不能接受负边。
于是我们有了引进了势这一概念。
我们将一个边的边权从原来简单的改为,且保证
为什么要这样改呢?
我们发现现在修改后,从到的路径长度为:
(其中为从到的一条路径的点的集合)
即为:
也就是说,我们求出加上势能后的最短路,仅需要再减去即可。
同时,我们可以证明加上势能后,不会对答案产生任何影响[证明方法类似于上文]。
接下来我们要考虑如何保证。
一开始,我们每条边都是正的。
将不等式移项。
发现和很像。
我们可以尝试每次将。
具体证明和代码可见传送门。
另外,其实这种方法正是借用了Johnson 全源最短路径算法的思想,有兴趣的可以去看一看。[是一种比Floyd更快的多源路径最短路算法]
level 2 zkw费用流
建议直接读zkw自己写的原文效果更佳
zkw费用流-by zkw
zkw费用流是一种不够稳定的算法,它在一些图上效率较高,但在部分数据上跑的很慢。
但是,在出题人不卡zkw费用流的情况下,该算法的效率还是很高的。
参考代码
#include<cstdio>
#include<cstring>
#include<queue>
#include<iostream>
#include<algorithm>
using namespace std;
#define MAXN 5000
#define MAXM 50000
#define INF 0x3f3f3f3f
int read()
{
int x=0,f=1;char s=getchar();
while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
return x*f;
}
struct E
{
int to,cap,cost,next;
}e[2*MAXM+5];
int head[MAXN+5],ecnt;
bool vis[MAXN+5];
int n,m,S,T,cnt,D;
void Clear()
{
ecnt = 0;
memset(head,-1,sizeof head);
}
void add(int fr,int to,int cap,int cost)
{
e[ecnt]=(E){to,cap,cost,head[fr]};
head[fr] = ecnt++;
e[ecnt]=(E){fr,0,-cost,head[to]};
head[to] = ecnt++;
}
bool modf(int s,int t)
{
//vis为访问到的点集,delta为修改值
int delta=INF;
for(int i=1;i<=n;i++)
if(vis[i])
for(int j=head[i];j!=-1;j=e[j].next)
if(e[j].cap&&!vis[e[j].to]&&e[j].cost<delta)delta=e[j].cost;
if(delta==INF)return 0;//所有点都访问到 退出
for(int i=1;i<=n;i++)
if(vis[i])
for(int j=head[i];j!=-1;j=e[j].next)
e[j].cost-=delta,e[j^1].cost+=delta;
D+=delta;
return 1;
}
int aug(int p,int t,int Flow,int &mincost)
{
if(p==t){mincost+=Flow*D;return Flow;}
vis[p]=1;
int Fl=Flow,tp;
for(int i=head[p];i!=-1;i=e[i].next)
{
if(e[i].cap&&!e[i].cost&&!vis[e[i].to])
{
tp=aug(e[i].to,t,min(Fl,e[i].cap),mincost);
e[i].cap-=tp,e[i^1].cap+=tp;
Fl-=tp;
if(!Fl)return Flow;
}
}
return Flow-Fl;
}
void MCMF(int s,int t,int &maxflow,int &mincost)
{
maxflow=mincost = 0;
int p=0;
do{
do{
memset(vis,0,sizeof vis);
maxflow+=p;
}while(p=aug(s,t,2e9,mincost));
}while(modf(s,t));
}
int main(){
Clear();
n=read(),m=read();
S=read(),T=read();
for(int j=1;j<=m;j++)
{
int u=read(),v=read(),cap=read(),w=read();
add(u,v,cap,w);
}
int ans1,ans2;
MCMF(S,T,ans1,ans2);
printf("%d %d",ans1,ans2);
}
level 3 原始对偶 (Primal-Dual) 算法
zkw费用流-by zkw
原始对偶算法是zkw算法和KM算法的结合。
是相对于两种算法而言相对算法性能较为稳定的算法。
比较适合在规模较大时写[代码量比前几个算法都要大]。
#include<cstdio>
#include<cstring>
#include<queue>
#include<iostream>
#include<algorithm>
using namespace std;
#define MAXN 5000
#define MAXM 50000
#define INF 0x3f3f3f3f
int read()
{
int x=0,f=1;char s=getchar();
while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
return x*f;
}
struct E
{
int to,cap,cost,next;
}e[3*MAXM+5];int head[MAXN*3+5] , ecnt;
int dis[MAXN*3+5],Q[MAXN*6+5];
bool vis[MAXN*3+5];
int n,m,S,T,cnt,D;
void Clear()
{
ecnt = 0;
memset(head,-1,sizeof head);
}
void add(int fr,int to,int cap,int cost)
{
e[ecnt]=(E){to,cap,cost,head[fr]};
head[fr] = ecnt++;
e[ecnt]=(E){fr,0,-cost,head[to]};
head[to] = ecnt++;
}
bool SPFA(int s,int t)
{
int xhd=0,xtal=1;
memset(vis,0,sizeof(vis));
memset(dis,0x3f,sizeof(dis));
Q[++xhd]=s;
dis[s]=0,vis[s]=1;
while (xhd>=xtal)
{
int cur=Q[xtal++];
for (int i=head[cur];i!=-1;i=e[i].next)
{
int nxt=e[i].to;
if (e[i].cap&&dis[nxt]>dis[cur]+e[i].cost)
{
dis[nxt]=dis[cur]+e[i].cost;
if (!vis[nxt])
{
if(dis[nxt]<dis[Q[xtal]])Q[--xtal]=nxt;
else Q[++xhd]=nxt;
}
vis[nxt]=1;
}
}
vis[cur]=0;
}
for(int i=1;i<=n;i++)
for(int j=head[i];j!=-1;j=e[j].next)
e[j].cost-=dis[e[j].to]-dis[i];
D+=dis[t];
return dis[t]<=1000000000;
}
int aug(int p,int t,int Flow,int &mincost)
{
if(p==t){mincost+=Flow*D;return Flow;}
vis[p]=1;
int Fl=Flow,tp;
for(int i=head[p];i!=-1;i=e[i].next)
{
if(e[i].cap&&!e[i].cost&&!vis[e[i].to])
{
tp=aug(e[i].to,t,min(Fl,e[i].cap),mincost);
e[i].cap-=tp,e[i^1].cap+=tp;
Fl-=tp;
if(!Fl)return Flow;
}
}
return Flow-Fl;
}
void MCMF(int s,int t,int &maxflow,int &mincost)
{
maxflow=mincost = 0;
int p=0;
while(SPFA(s,t))
{
do
{
memset(vis,0,sizeof(vis));
maxflow+=p;
}while(p=aug(s,t,2e9,mincost));
}
}
int main(){
Clear();
n=read(),m=read();
S=read(),T=read();
for(int j=1;j<=m;j++)
{
int u=read(),v=read(),cap=read(),w=read();
add(u,v,cap,w);
}
int ans1,ans2;
MCMF(S,T,ans1,ans2);
printf("%d %d",ans1,ans2);
}
level 4 一些小优化
仔细阅读了zkw大神的博客的同学肯定会发现,算法中的可以在一开始就预处理好。
这里我们是可以用来跑的,因为最开始所有负权边的流量都为0。
这样与处理好的费用流可以快左右。
其次,我们可以从汇点向源点跑,因为这时候算法就不会再走那些到不了汇点的边了。
这样与处理好的费用流又可以快左右。
这样一来,算法的效率就比较高了。
[以Luogu的模板题为例:开O2:474ms,不开:589ms(10个测试点总耗时)]
[然而代码量变大了]
#include<cstdio>
#include<cstring>
#include<queue>
#include<iostream>
#include<algorithm>
using namespace std;
#define MAXN 5000
#define MAXM 50000
#define INF 0x3f3f3f3f
#define FR first
#define SE second
int read()
{
int x=0,f=1;char s=getchar();
while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
return x*f;
}
struct E
{
int to,cap,cost,next;
}e[2*MAXM+5];int head[MAXN+5] ,ecnt;
int dis[MAXN+5],Q[MAXN*6+5],vis[MAXN+5];
int n,m,S,T,cnt,D;
void Clear()
{
ecnt = 0;
memset(head,-1,sizeof head);
}
void add(int fr,int to,int cap,int cost)
{
e[ecnt]=(E){to,cap,cost,head[fr]};
head[fr] = ecnt++;
e[ecnt]=(E){fr,0,-cost,head[to]};
head[to] = ecnt++;
}
void Dij(int s,int t)
{
for(int i=1;i<=n;i++)dis[i]=INF;
priority_queue<pair<int, int> > Q;
dis[t]=0;
Q.push(make_pair(0,t));
while(!Q.empty())
{
int cur=Q.top().SE,d=-Q.top().FR;
Q.pop();
if(d!=dis[cur])continue;
for (int i=head[cur];i!=-1;i=e[i].next)
{
int nxt=e[i].to;
if (e[i^1].cap&&dis[nxt]>dis[cur]-e[i].cost)
{
dis[nxt]=dis[cur]-e[i].cost;
Q.push(make_pair(-dis[nxt],nxt));
}
}
}
}
bool modf(int s,int t)
{
//vis为访问到的点集,delta为修改值
int delta=INF;
for(int i=1;i<=n;i++)
if(vis[i])
for(int j=head[i];j!=-1;j=e[j].next)
{
int v=e[j].to;
if(e[j].cap&&!vis[v])delta=min(delta,dis[v]+e[j].cost-dis[i]);
}
if(delta==INF)return 0;//所有点都访问到 退出
for(int i=1;i<=n;i++)
if(vis[i])dis[i]+=delta;
return 1;
}
int aug(int p,int t,int Flow,int &mincost)
{
if(p==t){mincost+=Flow*dis[S];return Flow;}
vis[p]=1;
int Fl=Flow,tp;
for(int i=head[p];i!=-1;i=e[i].next)
{
int v=e[i].to;
if(e[i].cap&&!vis[v]&&dis[p]==dis[v]+e[i].cost)
{
tp=aug(v,t,min(Fl,e[i].cap),mincost);
e[i].cap-=tp,e[i^1].cap+=tp;
Fl-=tp;
if(!Fl)return Flow;
}
}
return Flow-Fl;
}
void MCMF(int s,int t,int &maxflow,int &mincost)
{
maxflow=mincost = 0;
int p=0;
Dij(s,t);
do{
do{
memset(vis,0,sizeof(vis));
maxflow+=p;
}while(p=aug(s,t,2e9,mincost));
}while(modf(s,t));
}
int main(){
Clear();
n=read(),m=read();
S=read(),T=read();
for(int j=1;j<=m;j++)
{
int u=read(),v=read(),cap=read(),w=read();
add(u,v,cap,w);
}
int ans1,ans2;
MCMF(S,T,ans1,ans2);
printf("%d %d",ans1,ans2);
}

浙公网安备 33010602011771号