[学习笔记]网络流/费用流总结

这几天集中学习了一下有关网络流/费用流的问题。
自己之前也没有较为深入地研究过,只是打了几道板题然后就咕咕了。
[补]这篇博客本来该一周之前就发布的,结果忘掉了。

模板

网络流问题

level 1 EK 算法

主要算法:多次BFS(增广)
EK 算法全名Edmonds-Karp算法。
是网络流当中最基础的算法。[当然,也是均摊下来跑得最慢的一种算法]
主要用到了残余网络的一些性质。
每次找到一条可增广的最短(这里指边数最小)的路径,对其增广,直到不能增广为止。
这里不详细讲解.
复杂度上限为O(nm2)O(nm^2)
EK算法算是最通用的一种算法,只需将BFS改为SPFA就可以做费用流了。
复杂度不变。
[不建议使用该算法]

level 2 Dinic算法

i.简介:

主要算法:DFS+分层图
主要优化了EK算法中每次只能找一条增广路径的劣势。
dinic算法在EK算法的基础上增加了分层图的概念,我们将其按离源点的最短距离的大小进行分层。
显然,我们要找的增广路上的点一定属于不同的层。
这样我们就可在一次增广中找到多条路径了。

ii.复杂度:

在普通情况下,Dinic时间复杂度为O(V2E)O(V^2E)
在二分图中,Dinic算法时间复杂度为O(VE)O(\sqrt{V}E)
[具体请参考其他资料]

iii.优化:

Dinic算法最主要优化的就是当前弧优化

当前弧优化:每一次dfsdfs增广时不从第一条边开始,而是用一个数组curcur记录点uu之前循环到了哪一条边,以此来加速

[强烈建议加上这个优化]

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它死了
事实证明SPFASPFA任何优化下,都能够被毒瘤的出题人卡超时。
[关于SPFA的优化]
然而,费用流是有负边[这里指反边]的。堆优化dijkstra并不能接受负边。
于是我们有了引进了这一概念。
我们将一个边的边权从原来简单的wu,vw_{u,v}改为wu,v+huhvw_{u,v}+h_u-h_v,且保证wu,v+huhv0w_{u,v}+h_u-h_v≥0
为什么要这样改呢?
我们发现现在修改后,从11nn的路径长度为:
(wp1+wp2+...+wpk)+(hp1hp2)+(hp2hp3)+...(hpk1hpk)(w_{p_1}+w_{p_2}+...+w_{p_k})+(h_{p_1}-h_{p_2})+(h_{p_2}-h_{p_3})+...(h_{p_{k-1}}-h_{p_k})
(其中pp为从11nn的一条路径的点的集合)
即为:
(wp1+wp2+...+wpk)+hp1hpk(w_{p_1}+w_{p_2}+...+w_{p_k})+h_{p_1}-h_{p_k}
也就是说,我们求出加上势能后的最短路,仅需要再减去hpkhp1h_{p_k}-h_{p_1}即可。
同时,我们可以证明加上势能后,不会对答案产生任何影响[证明方法类似于上文]。
接下来我们要考虑如何保证wu,v+huhv0w_{u,v}+h_u-h_v≥0
一开始,我们每条边都是正的。
将不等式移项wu,v+huhvw_{u,v}+h_u≥h_v
发现和wu,v+disudisvw_{u,v}+dis_u≥dis_v很像。
我们可以尝试每次将hi+=disih_i+=dis_i
具体证明和代码可见传送门
另外,其实这种方法正是借用了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大神的博客的同学肯定会发现,算法中的DiD_i可以在一开始就预处理好。
这里我们是可以用DijstraDijstra来跑的,因为最开始所有负权边的流量都为0。
这样与处理好的费用流可以快400ms400ms左右。
其次,我们可以从汇点向源点跑DijstraDijstra,因为这时候算法就不会再走那些到不了汇点的边了。
这样与处理好的费用流又可以快200ms200ms左右。
这样一来,算法的效率就比较高了。
[以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);
}
posted @ 2019-01-06 21:34  C20190406  阅读(860)  评论(0)    收藏  举报