[理性愉悦]最大流算法学习

本文总结了一些OI中常用和不常用的最大流算法,并对运行效率进行简单测试。主要是闲着无聊没事干

Tip:除了Dinic以外其他算法我都是现学现写的,理解/代码上可能有各种错误,欢迎拍砖qwq

0. Edmonds-Karp

由于这种方法实在太慢了,这里不提。

不过这个做法告诉了我们一个重要道理:网络流找增广路要加反向边。

1. Dinic

这个经典网络流算法相信大家都耳熟能详。

我们在残余网络上bfs,把访问到的点按层标号,接着将跨层的边进行dfs增广。(当然一句话讲的也不是很清楚,不会的自行百度吧)

正确的dinic分层姿势是从T开始倒着分层,从S开始正着分层复杂度似乎不变,但是从T开始跑的快一点。

Dinic主要有两个优化,一个是当前弧优化(即不重复访问一条边),另一个是当一个点无法增广时就放弃这个点。还有一个优化比较玄学,因为bfs很慢,所以我们随便搜出一条路就返回,复杂度全看脸,详见代码中的注释。

#define SZ 666666
typedef long long ll;
int fst[SZ],nxt[SZ],vb[SZ],cap[SZ],ff[SZ],M=1,N=0;
#define ad_de ad_dl
void _ad_dl(int a,int b,int c) {++M;nxt[M]=fst[a];fst[a]=M;vb[M]=b;cap[M]=c;}
void ad_dl(int a,int b,int c)
{_ad_dl(a,b,c); _ad_dl(b,a,0);}
int S,T,q[SZ],d[SZ];
bool bfs()
{
    memset(d,-1,4*(N+1));
    d[T]=0; q[1]=T; int h=1,t=2;
    while(h!=t)
    {
        int cur=q[h++];
        for(int e=fst[cur];e;e=nxt[e])
        {
            int b=vb[e];
            if(d[b]==-1&&cap[e^1])
            {
                d[b]=d[cur]+1, q[t++]=b;
                if(b==S) return 1; //这是玄学 
            }
        }
    }
    return d[S]!=-1;
}
int dfs(int x,int f)
{
    if(f<=0) return 0;
    if(x==T) return f;
    int ca=0;
    for(int& e=fst[x];e;e=nxt[e]) //当前弧优化 
    {
        int b=vb[e];
        if(d[b]+1!=d[x]) continue;
        int w=dfs(b,(cap[e]<f-ca)?cap[e]:(f-ca));
        cap[e]-=w; cap[e^1]+=w; ca+=w;
        if(ca==f) break;
    }
    if(!ca) d[x]=-1; //另一个优化 
    return ca;
}
#define inf 2e9+3
int dinic()
{
    int ans=0;
    memcpy(ff,fst,4*(N+1));
    while(bfs())
    {
        //当前弧优化
        ans+=dfs(S,inf);
        memcpy(fst,ff,4*(N+1));
    }
    return ans;
}

理论时间复杂度为$O(n^2m)$,据说寻找增广路时使用LCT可以优化到$O(nmlog(n))$([理性愉悦]最大流算法学习)。

2. Sap

考虑仍然像dinic一样把从T开始反着把图分层,这回我们增广时采用dfs,只进行单路增广,只走相邻层的边,找到一条增广路就增广返回起点。

这个做法听上去非常naive,但是可以加入两个优化。

Dinic的时候我们每次增广完一堆路径,这时候我们会重新跑一遍bfs,然后再增广一堆路径,而Sap不需要重新跑一遍bfs,而是当某个点找不到增广路时,把它到T的层数用它所有有流量的后继层数最小值+1来更新再返回,这样就避免了重新跑bfs。

此外还有一个叫gap的玄学优化,就是说如果从S到T中间有一个层一个点也没有,那么这个层就是一个“gap”,阻止了流量的传输,那么我们就可以直接退出bfs节省时间。

当然当前弧优化也是可以使用的。实际写的时候并不需要真的dfs,而是记录每个点的pre(反正增广也要用),直接回溯。

#define SZ 666666
int fst[SZ],nxt[SZ],vb[SZ],cap[SZ],ff[SZ],M=1,N=0;
#define ad_de ad_dl
void _ad_dl(int a,int b,int c) {++M;nxt[M]=fst[a];fst[a]=M;vb[M]=b;cap[M]=c;}
void ad_dl(int a,int b,int c)
{_ad_dl(a,b,c); _ad_dl(b,a,0);}
int S,T,q[SZ],d[SZ],gap[SZ];
int pre[SZ],low[SZ],pe[SZ];
int sap()
{
    for(int i=0;i<=N;++i) d[i]=N,ff[i]=fst[i];
    d[T]=0; q[1]=T; int h=1,t=2;
    while(h^t)
    {
        int x=q[h++];
        for(int e=fst[x];e;e=nxt[e])
        {
            int b=vb[e];
            if(d[b]==N&&cap[e^1])
                d[b]=d[x]+1, q[t++]=b;
        }
    }
    for(int i=1;i<=N;++i) ++gap[d[i]];
    int x=S,ans=0;low[S]=2e9;
    while(d[S]<N)
    {
        int ce=-1;
        for(int& e=fst[x];e;e=nxt[e]) //当前弧优化 
        {
            if(!cap[e]||d[vb[e]]+1!=d[x]) continue;
            ce=e; break;
        }
        if(ce==-1)
        {
            --gap[d[x]];
            if(!gap[d[x]]) break; //gap优化 
            d[x]=N; fst[x]=ff[x]; //回退当前弧 
            for(int e=fst[x];e;e=nxt[e])
                (cap[e]&&d[vb[e]]+1<d[x])?(d[x]=d[vb[e]]+1):0; //sap
            ++gap[d[x]];
            if(x!=S) x=pre[x]; //回溯 
        }
        else
        {
            int b=vb[ce]; pre[b]=x;pe[b]=ce;
            low[b]=(low[x]<cap[ce])?low[x]:cap[ce];
            x=b; if(x!=T) continue;
            //找到了增广路
            ans+=low[x];
            for(int p=x;p!=S;p=pre[p])
                cap[pe[p]]-=low[x],cap[pe[p]^1]+=low[x];
            x=S;
        }
    }
    return ans;
}

它的理论时间复杂度仍然为$O(n^2m)$。

3. HLPP

http://blog.****.net/hechenghai/article/details/42024683

https://en.wikipedia.org/wiki/Push%E2%80%93relabel_maximum_flow_algorithm

这个算法全称叫最高标号预流推进,和Sap类似,仍然从T开始给每个点一个距离标号,无法增广时就更新标号。开始每个点的距离标号可以赋为0,也可以正常bfs。

既然叫“最高标号”预流推进算法,这个算法按标号从大到小维护了一个优先队列,表示要增广的点。

那么什么叫“预流推进”呢?“预流”就表示想往外流,每次考虑要增广一条边(a,b),那么a的预流就减掉这条边的流量,b的预流加上这条边的流量。

这个算法主体用bfs增广,增广一个点时,考虑每条通往下层的边,按上面的做法推流,推到了没进队的点就加入优先队列,当这个点的预流为0时它的历史使命就完成了。如果一个点想要推流却啥也没推出去,那么就更新它的标号。最后检查这个点的预流是否为0,没0继续加入队列。

注意S和T除了一开始S加入队列外不应该加入队列。开始将S的预流赋为∞,该推的流都推完以后T的预流就是答案。

当前弧优化在这个算法中似乎不适用。

#include <iostream>
#include <stdio.h>
#include <math.h>
#include <string.h>
#include <time.h>
#include <stdlib.h>
#include <string>
#include <bitset>
#include <vector>
#include <set>
#include <map>
#include <queue>
#include <algorithm>
#include <sstream>
#include <stack>
#include <iomanip>
using namespace std;
#define pb push_back
#define mp make_pair
typedef pair<int,int> pii;
typedef long long ll;
typedef double ld;
typedef vector<int> vi;
#define fi first
#define se second
#define fe first
#define FO(x) {freopen(#x".in","r",stdin);freopen(#x".out","w",stdout);}
#define Edg int M=0,fst[SZ],vb[SZ],nxt[SZ];void ad_de(int a,int b){++M;nxt[M]=fst[a];fst[a]=M;vb[M]=b;}void adde(int a,int b){ad_de(a,b);ad_de(b,a);}
#define Edgc int M=0,fst[SZ],vb[SZ],nxt[SZ],vc[SZ];void ad_de(int a,int b,int c){++M;nxt[M]=fst[a];fst[a]=M;vb[M]=b;vc[M]=c;}void adde(int a,int b,int c){ad_de(a,b,c);ad_de(b,a,c);}
#define es(x,e) (int e=fst[x];e;e=nxt[e])
#define esb(x,e,b) (int e=fst[x],b=vb[e];e;e=nxt[e],b=vb[e])
#define VIZ {printf("digraph G{\n"); for(int i=1;i<=n;i++) for es(i,e) printf("%d->%d;\n",i,vb[e]); puts("}");}
#define VIZ2 {printf("graph G{\n"); for(int i=1;i<=n;i++) for es(i,e) if(vb[e]>=i)printf("%d--%d;\n",i,vb[e]); puts("}");}
#define SZ 666666
int fst[SZ],nxt[SZ],vb[SZ],cap[SZ],M=1,N=0;
#define ad_de ad_dl
void _ad_dl(int a,int b,int c) {++M;nxt[M]=fst[a];fst[a]=M;vb[M]=b;cap[M]=c;}
void ad_dl(int a,int b,int c)
{_ad_dl(a,b,c); _ad_dl(b,a,0);}
int S,T,q[SZ],d[SZ],ex[SZ];
bool inq[SZ]; priority_queue<pii> pq;
#define gq(x) ((x)!=S&&(x)!=T&&ex[x]&&!inq[x])
int hlpp()
{
    for(int i=0;i<=N;++i) d[i]=N+2;
    d[T]=0; q[1]=T; int h=1,t=2;
    while(h^t)
    {
        int x=q[h++];
        for(int e=fst[x];e;e=nxt[e])
        {
            int b=vb[e];
            if(d[b]==N+2&&cap[e^1])
                d[b]=d[x]+1, q[t++]=b;
        }
    }
    d[S]=N+2;
    pq.push(pii(d[S],S)); inq[S]=1; ex[S]=2e9;
    while(!pq.empty())
    {
        int x=pq.top().se; pq.pop(); inq[x]=0;
        bool p=0;
        for(int e=fst[x];e;e=nxt[e])
        {
            int b=vb[e];
            if(!cap[e]||(x!=S&&d[b]+1!=d[x])) continue;
            p=1; int f=(cap[e]<ex[x])?cap[e]:ex[x];
            ex[x]-=f; ex[b]+=f;
            cap[e]-=f; cap[e^1]+=f;
            if(gq(b)) inq[b]=1,pq.push(pii(d[b],b));
            if(!ex[x]) break;
        }
        if(!p)
        {
            d[x]=N+2;
            for(int e=fst[x];e;e=nxt[e])
                (cap[e]&&d[vb[e]]+1<d[x])?(d[x]=d[vb[e]]+1):0;
        }
        if(gq(x)) inq[x]=1,pq.push(pii(d[x],x));
    }
    return ex[T];
}
namespace FF
{
char ch,B[1<<15],*S=B,*T=B;
#define getc() (S==T&&(T=(S=B)+fread(B,1,1<<15,stdin),S==T)?0:*S++)
#define isd(c) (c>='0'&&c<='9')
int aa,bb;int F(){
    while(ch=getc(),!isd(ch)&&ch!='-');ch=='-'?aa=bb=0:(aa=ch-'0',bb=1);
    while(ch=getc(),isd(ch))aa=aa*10+ch-'0';return bb?aa:-aa;
}
}
#define gi FF::F()
int main()
{
    N=gi; int m=gi;
    S=1; T=N;
    for(int i=1;i<=m;i++)
    {
        int x=gi,y=gi,z=gi;
        ad_dl(x,y,z);
    }
    printf("%d\n",hlpp());
}

UPD:注意!这份代码是错误的,原因我现在还不清楚,反正是错的,下面有关HLPP的测试结果请忽略。

理论时间复杂度比上面两个优秀!为$O(n^2\sqrt{m})$,这个界是紧的。

4. Goldberg-Rao

http://www.cs.princeton.edu/courses/archive/fall06/cos528/handouts/Goldberg-Rao.pdf

http://www.cs.tau.ac.il/~haimk/adv-alg-2013/goldbergrao.pdf

https://people.orie.cornell.edu/dpw/orie633/LectureNotes/lecture10.pdf

首先我们需要知道,dinic的复杂度十分玄学,在容量为1的图上跑的飞快,经过一番严谨的证明可以证出上界是$O(min(n^{2/3},m^{1/2})m)$(详见维基)。

我们考虑将dinic算法改造一番将它在容量大的时候也跑的很快。

在开始之前,我们设$\Lambda=min(n^{2/3},m^{1/2})$,最多能增广的量不超过F,$\Delta=\lceil{F}/\Lambda\rceil$。

考虑形式化地定义dinic算法,我们定义一条边u->v是好的,当且仅当$d(v)=d(u)+l$,其中l表示这条边的长度。在原始dinic算法中,l恒为1,但在这个算法中,l可能为0或1。d的定义同理,形式化地,$d(T)=0$,对于每个点v,$d(v)\leq{d(w)+l(v~to~w)}$,并且等号能取到。

我们定义一条边为大的,当且仅当它的残余容量$\geq3\Delta$;一条边为特别的,这个条件比较多,有三个条件:残余容量在$[2\Delta,3\Delta)$,两端点距离标号相等,它的反向边残余容量$\geq3\Delta$。

那么我们现在可以定义一条边的l了,定义十分简单,如果这条边是大的或者特别的,l为0,否则为1。

仔细一想你会觉得有些不对,l依赖“特别”来定义,“特别”依赖距离标号来定义,而距离标号却又依赖l来计算...但是事实上我们可以先忽略特别的边,即只考虑小的边将其l设为0,这样计算出距离标号之后再重新计算一遍l,因为特别的边显然不会改变距离标号。

然后我们开始先定一个F的上界,为了简化我们设U为每条边的最大容量,那么就开始设$F=mU$。

算法流程大致如下:

初始化F
while F>=1:
  计算△
  重复5Λ次操作:
    初始化距离标号和边的l值
    在好的边上找一个堵塞的或者流量为△的流
    增广
  F/=2

“堵塞”的流就表示是增广出来满的一个流。是不是看起来十分简单呢?

我们还遗漏了一个重要细节。在正常dinic中,l都为1,增广轻松愉快。但在这个算法中l可能等于0,那么有可能出现流量循环的情况,因为同一层的点间可能会有流量。

流量循环?Tarjan!我们在好的边上跑一遍tarjan,把每个强连通分量缩成一个点,那么就不会有什么循环的情况出现了!

那么缩完点之后的流量怎么办?我们在强连通分量里随便找个点r当做根,同样在好边上dfs两遍搞出一棵In Tree,一棵Out Tree。盗图一张:

[理性愉悦]最大流算法学习

如果有了流量,我们通过In Tree上的边流给r,再从Out Tree上的边流出去,这样就搞完啦!

不过还有一个小优化,考虑每次F/=2显得非常蠢,我们考虑想办法乱搞求出残余流量的一个上界,进行估价,每次F=min(F/2,计算出的上界)。

我们考虑最小割=最大流,那么考虑算出一个比较小的割。考虑在原图上bfs求出距离标号,然后每次考虑对于每个k,把距离标号为k和k+1的点之间的边全部割掉,这显然是一个合法的割,虽然不是很小,我们就用这个进行估价。同时,开始的F也可以设为这个值。

当前弧优化还是可以继续使用的。另外如果找到的堵塞流流量为0,可以直接break。

正确性和复杂度证明留作习题

注意下面这份代码可能有各种各样的错误,数据中的边权都是随机的,所以增广一次就够了...更强的数据我并不会造,就先这样好了...

#include <iostream>
#include <stdio.h>
#include <math.h>
#include <string.h>
#include <time.h>
#include <stdlib.h>
#include <string>
#include <bitset>
#include <vector>
#include <set>
#include <map>
#include <queue>
#include <algorithm>
#include <sstream>
#include <stack>
#include <iomanip>
using namespace std;
#define pb push_back
#define mp make_pair
typedef pair<int,int> pii;
typedef long long ll;
typedef double ld;
typedef vector<int> vi;
#define fi first
#define se second
#define fe first
#define FO(x) {freopen(#x".in","r",stdin);freopen(#x".out","w",stdout);}
#define Edg int M=0,fst[SZ],vb[SZ],nxt[SZ];void ad_de(int a,int b){++M;nxt[M]=fst[a];fst[a]=M;vb[M]=b;}void adde(int a,int b){ad_de(a,b);ad_de(b,a);}
#define Edgc int M=0,fst[SZ],vb[SZ],nxt[SZ],vc[SZ];void ad_de(int a,int b,int c){++M;nxt[M]=fst[a];fst[a]=M;vb[M]=b;vc[M]=c;}void adde(int a,int b,int c){ad_de(a,b,c);ad_de(b,a,c);}
#define es(x,e) (int e=fst[x];e;e=nxt[e])
#define esb(x,e,b) (int e=fst[x],b=vb[e];e;e=nxt[e],b=vb[e])
#define VIZ {printf("digraph G{\n"); for(int i=1;i<=n;i++) for es(i,e) printf("%d->%d;\n",i,vb[e]); puts("}");}
#define VIZ2 {printf("graph G{\n"); for(int i=1;i<=n;i++) for es(i,e) if(vb[e]>=i)printf("%d--%d;\n",i,vb[e]); puts("}");}
#define SZ 666666
#define FEdge \
int M=1,fst[SZ],vb[SZ],cap[SZ],nxt[SZ],va[SZ];\
void ad_de_(int a,int b,int c)\
{++M; nxt[M]=fst[a]; vb[M]=b; cap[M]=c; fst[a]=M; va[M]=a;}\
void ad_de(int a,int b,int c)\
{ad_de_(a,b,c); ad_de_(b,a,0);}
#define GetD(l) \
int qs[SZ];\
bool inq[SZ];\
void spfa()\
{\
    int h=0,t=1;qs[0]=T;\
    for(int i=1;i<=n;i++) d[i]=1e9;\
    d[T]=0; inq[T]=1;\
    while(h^t)\
    {\
        int x=qs[h++];\
        for(int e=fst[x];e;e=nxt[e])\
        {\
            if(cap[e^1]);else continue;\
            int y=d[x]+l(e^1),b=vb[e];\
            if(d[b]<=y) continue;\
            d[b]=y; if(inq[b]) continue;\
            inq[b]=1; qs[t++]=b;\
        }\
        inq[x]=0;\
    }\
}
int n,m,S,T;
namespace dn
{
int T,d[SZ],M=1,fst[SZ],vb[SZ],cap[SZ],nxt[SZ],l[SZ];
int addedge(int a,int b,int c,int g)
{++M; nxt[M]=fst[a]; vb[M]=b; cap[M]=c; fst[a]=M; l[M]=g; return M;}
int dfs(int x,int f)
{
    if(f<=0) return 0;
    if(x==T) return f;
    int ca=0;
    for(int& e=fst[x];e;e=nxt[e])
    {
        int b=vb[e];
        if(d[b]+l[e]!=d[x]) continue;
        int w=dfs(b,min(cap[e],f-ca));
        cap[e]-=w; cap[e^1]+=w; ca+=w;
        if(ca==f) break;
    }
    if(!ca) d[x]=-233;
    return ca;
}
}
namespace ng
{
FEdge int d[SZ];
namespace og
{
int d[SZ],sm[SZ]; GetD(1+0*)
int gj()
{
    spfa();
    int mx=d[S];
    if(mx==1e9) return 0;
    for(int i=0;i<=mx;++i) sm[i]=0;
    for(int i=1;i<=n;i++)
    {
        for(int e=fst[i];e;e=nxt[e])
        {
            int b=vb[e];
            if(!cap[e]||d[b]+1!=d[i]) continue;
            sm[d[i]]+=cap[e];
        }
    }
    int ans=2147483647;
    for(int i=1;i<=mx;++i)
        ans=min(ans,sm[i]);
    return ans;
}
}
ll F; int L,D,vs[SZ],ans=0;
bool bg(int e) {return cap[e]>=D+D+D;}
bool fb(int e) {return cap[e]<D+D+D;}
int val(int e)
{return !(bg(e)||(cap[e]>=D+D&&cap[e]<D+D+D
&&d[va[e]]==d[vb[e]]&&cap[e^1]>=D+D+D));}
GetD(fb)
int low[SZ],dfn[SZ],cur=0;
bool ins[SZ];
int ss[SZ],sn=0,bl[SZ],C=0;
void tarjan(int x)
{
    dfn[x]=low[x]=++cur; ss[++sn]=x; ins[x]=1;
    for esb(x,e,b)
    {
        if(vs[e]) continue;
        if(ins[b]) low[x]=min(low[x],dfn[b]);
        else if(!dfn[b])
            tarjan(b),low[x]=min(low[x],low[b]);
    }
    if(dfn[x]!=low[x]) return;
    int cs=0; ++C;
    while(1)
    {
        ++cs; int t=ss[sn--];
        bl[t]=C; ins[t]=0;
        if(t==x) break;
    }
}
int fa[SZ],tmp[SZ],oi[SZ]/*in-out*/;
void dfs(int x,int g)
{
    for esb(x,e,b)
    {
        if(bl[b]!=g||fa[b]) continue;
        fa[b]=x; dfs(b,g);
        cap[e]+=oi[b]; cap[e^1]-=oi[b];
        oi[x]+=oi[b]; oi[b]=0;
    }
}
//大概有错 
void doit()
{
    L=min(pow(n,2.0/3),pow(m,1.0/2));
    if(L<1) L=1; F=1e9;
    int tm=0;
    while(F>=1)
    {
        D=ceil(F/(ld)L);
        for(int i=1;i<=5*L;++i)
        {
            spfa();
            for(int i=2;i<=M;++i) vs[i]=val(i);
            sn=cur=C=0;
            for(int i=1;i<=n;i++)
                dfn[i]=low[i]=ins[i]=oi[i]=0;
            for(int i=1;i<=n;i++)
                if(!low[i]) tarjan(i);
            if(bl[S]==bl[T]) break;
            dn::M=1; dn::T=bl[T];
            for(int i=1;i<=C;i++) dn::fst[i]=0;
            for(int i=1;i<=n;i++) dn::d[bl[i]]=d[i];
            for(int i=2;i<=M;i++)
            {
                int a=va[i],b=vb[i];
                if(bl[a]!=bl[b]) tmp[i]=dn::addedge(bl[a],bl[b],cap[i],vs[i]);
            }
            ll cur=dn::dfs(bl[S],D); ans+=cur;
            if(!cur) break;
            for(int i=1;i<=n;i++)
                for esb(i,e,b)
                    if(bl[i]!=bl[b])
                    {
                        ll l=cap[e]-dn::cap[tmp[e]];
                        oi[i]-=l; cap[e]-=l;
                    }
            for(int i=1;i<=n;i++)
                if(!low[i]) fa[i]=-1,dfs(i,bl[i]);
        }
        F=min(int(F/2),og::gj());
    }
    printf("%d\n",ans);
}
}
namespace FF
{
char ch,B[1<<15],*S=B,*T=B;
#define getc() (S==T&&(T=(S=B)+fread(B,1,1<<15,stdin),S==T)?0:*S++)
#define isd(c) (c>='0'&&c<='9')
int aa,bb;int F(){
    while(ch=getc(),!isd(ch)&&ch!='-');ch=='-'?aa=bb=0:(aa=ch-'0',bb=1);
    while(ch=getc(),isd(ch))aa=aa*10+ch-'0';return bb?aa:-aa;
}
}
#define gi FF::F()
int main()
{
    n=gi,m=gi; S=1; T=n;
    for(int i=1;i<=m;++i)
    {
        int a,b,c;
        a=gi,b=gi,c=gi;
        ng::ad_de(a,b,c);
    }
    ng::doit();
}

理论复杂度为$O(min(n^{2/3},m^{1/2})mlog(n)log(mU))$,其中U表示边的最大容量。

测试数据中n=3000,m=200000,11组数据,时限2s,采用各种姿势生成,目测挺强的。如果按理论复杂度这些算法一个也过不了

sap:100分/492ms,dinic:100分/508ms,假的goldberg-rao:100分/1812ms,HLPP:45~54分(写挂了)

终于填上了这个大坑好高兴啊,过几天开新坑。