网络流简记

更新日志 2024/12/31:开工。添加网络流概念以及EK算法

2025/01/01:添加Dinic算法,最小割与费用流

2025/01/02:添加最小割方案求法

2025/01/02:添加上下界网络流问题


概念

官方定义

OI-wiki

网络

一种特殊有向图,有一个源点 \(s\) 与汇点 \(t\)

图中每一条边都具有容量 \(c\),也就是流经流量上限。不存在的边 \(c=0\)

可以视作流水,从源点开始进水(无限或有限),通过一条条边流开,每条边的尺寸限定了流量。

要求所有流都要汇入汇点。也就是流量守恒。

一个整体,大概就是所有有流水的边。

两个点集合 \(S,T\),满足 \(S\cup T=V\)\(S\cap T=\varnothing\),同时 \(s\in S,t\in T\)

一个割的容量是 \(\sum\limits_{u\in S}\sum\limits_{v\in T}c(u,v)\)

形象化的,就是将点集分成两个集合,割的容量就是连接两个集合的边权之和。

最大流

问题

求一个网络的最大流量。

也就是每条边流量之和。

Ford-Fulkerson 增广

贪心思想。

每次找到一条从 \(s\)\(t\) 的路径,称之为增广路,给它加上 \(w\),也就是增广操作,那么流量就加上了 \(w\)

为了保证最优,我们加入反悔操作。具体的,每一条边都建立一条反边,反边空闲的流量就是正边已走的流量,这样走反边就相当于反悔了。

正确性证明略。

EK算法

直接模拟 Ford-Fulkerson 算法。

具体的,每次找一条 \(s-t\) 最短路(不是权值和最小,单纯的经过路程最小),保证每一条可行路径均被取到,然后每找到一条路径就操作一次:

  • 给路径中每一条边的流量加上可行的最大值(路径中最小的剩余流量),同时给答案也加上。
  • 别忘了给反边相应减去对应值。

具体地,BFS 即可。证明略。

复杂度 \(O(nm^2)\)

模板

struct EK{
    int n,cnt;
    int hd[N];
    struct edge{
        int ne,to;
        ll cap,flow;
    }ed[M*2];
    int p[N];ll a[N];
    queue<int> q;

    void init(int num){
        n=num;
        cnt=0;
        rep(i,0,n)hd[i]=-1;
    }
    void adde(int a,int b,ll cap){
        ed[cnt].ne=hd[a];hd[a]=cnt;
        ed[cnt].to=b;
        ed[cnt].cap=cap;
        ed[cnt].flow=0;
        cnt++;
        ed[cnt].ne=hd[b];hd[b]=cnt;
        ed[cnt].to=a;
        ed[cnt].cap=0;
        ed[cnt].flow=0;
        cnt++;
    }
    bool bfs(int s,int t){
        rep(i,0,n)a[i]=0;
        while(!q.empty())q.pop();
        a[s]=INF;
        q.push(s);
        while(!q.empty()){
            int now=q.front();q.pop();
            for(int e=hd[now];~e;e=ed[e].ne){
                int nxt=ed[e].to;
                if(!a[nxt]&&ed[e].cap>ed[e].flow){
                    a[nxt]=min(a[now],ed[e].cap-ed[e].flow);
                    p[nxt]=e;
                    q.push(nxt);
                }
            }
            if(a[t])return 1;
        }
        return 0;
    }
    ll maxflow(int s,int t){
        ll res=0;
        while(bfs(s,t)){
            res+=a[t];
            for(int now=t;now!=s;now=ed[p[now]^1].to){
                ed[p[now]].flow+=a[t];
                ed[p[now]^1].flow-=a[t];
            }
        }
        return res;
    }
};

Dinic算法

首先重建分层图。

具体地,先BFS一遍分层,然后每个点只保留其到下一层的边,而删去其到上一层或同层的边。

随后,在分层图上,每次找到流量最大的增广流(即为阻塞流。这里的增广流是指整个流的路径并集,而非单条路径),将其加入答案、更新流。

直到不存在阻塞流为止,此时就是答案。

当前弧优化

这个优化可以保证复杂度。

具体地,我们维护每个点第一个还有余量的出边,防止每次都从头开始遍历。

表现在代码中,我们只需要在遍历边的时候加上引用即可。

但注意求新的阻塞流的时候需要复原。以上优化仅用于求同一阻塞流时。

模板

struct Dinic{
    int n,cnt;
    int hd[N],fr[N];
    struct edge{
        int ne,to;
        ll cap,flow;
    }ed[M*2];
    int dp[N];
    queue<int> q;

    void init(int num){
        n=num;
        cnt=0;
        rep(i,0,n)hd[i]=-1;
    }
    void adde(int a,int b,ll cap){
        ed[cnt].ne=hd[a];hd[a]=cnt;
        ed[cnt].to=b;
        ed[cnt].cap=cap;
        ed[cnt].flow=0;
        cnt++;
        ed[cnt].ne=hd[b];hd[b]=cnt;
        ed[cnt].to=a;
        ed[cnt].cap=0;
        ed[cnt].flow=0;
        cnt++;
    }
    int bfs(int s,int t){
        rep(i,0,n)dp[i]=0;
        dp[s]=1;
        q.push(s);
        while(!q.empty()){
            int now=q.front();q.pop();
            for(int e=hd[now];~e;e=ed[e].ne){
                int nxt=ed[e].to;
                if(ed[e].cap>ed[e].flow&&!dp[nxt]){
                    dp[nxt]=dp[now]+1;
                    q.push(nxt);
                }
            }
        }
        return dp[t];
    }
    ll dfs(int now,int t,ll flow){
        if(now==t||!flow)return flow;
        ll res=0;
        for(int &e=fr[now];~e;e=ed[e].ne){
            int nxt=ed[e].to;
            if(ed[e].cap>ed[e].flow&&dp[nxt]==dp[now]+1){
                ll f=dfs(nxt,t,min(flow-res,ed[e].cap-ed[e].flow));
                res+=f;
                ed[e].flow+=f;
                ed[e^1].flow-=f;
                if(res==flow)break;
            }
        }
        return res;
    }
    ll maxflow(int s,int t){
        ll res=0;
        while(bfs(s,t)){
            rep(i,0,n)fr[i]=hd[i];
            res+=dfs(s,t,INF);
        }
        return res;
    }
};

最小割

问题

求容量最小的割。

解决

根据最大流最小割定理,最大流就是最小割。证明略。

方案

从源点开始,每次走有余量的边,走到的点都在 \(S\) 中。

费用流

问题

每条边多了一个属性单位费用 \(w\)

流经的费用就是 \(f(u,v)\times w(u,v)\)

\(w(u,v)=-w(v,u)\)

要求最大流前提下最小化费用的问题就是最小费用最大流。

SPP 算法

\(\text{SPP}\) 贪心算法,每次找增广路的时候优先找费用最小的即可。

SPFA

通过 \(\text{SPFA}\) 解决负边权最短路问题。注意不可以有负环。

直接把两个算法中找增广路部分(\(\text{BFS}\))换成 \(\text{SPFA}\) 即可。

EK模板

其他部分没有什么变化,最后回溯时顺便加上代价即可。

struct SPP_EK{
    int n,cnt;
    int hd[N];
    struct edge{
        int ne,to;
        ll cap,flow,val;
    }ed[M*2];
    int p[N];ll a[N];ll dis[N];
    queue<int> q;bool inq[N];

    void init(int num){
        n=num;
        cnt=0;
        rep(i,0,n)hd[i]=-1;
    }
    void adde(int a,int b,ll cap,ll val){
        ed[cnt].ne=hd[a];hd[a]=cnt;
        ed[cnt].to=b;
        ed[cnt].cap=cap;
        ed[cnt].flow=0;
        ed[cnt].val=val;
        cnt++;
        ed[cnt].ne=hd[b];hd[b]=cnt;
        ed[cnt].to=a;
        ed[cnt].cap=0;
        ed[cnt].flow=0;
        ed[cnt].val=-val;
        cnt++;
    }
    bool spfa(int s,int t){
        rep(i,0,n)a[i]=0,dis[i]=INF,inq[i]=0;
        a[s]=INF;dis[s]=0;
        q.push(s);
        inq[s]=1;
        while(!q.empty()){
            int now=q.front();q.pop();
            inq[now]=0;
            for(int e=hd[now];~e;e=ed[e].ne){
                int nxt=ed[e].to;
                if(dis[nxt]>dis[now]+ed[e].val&&ed[e].cap>ed[e].flow){
                    dis[nxt]=dis[now]+ed[e].val;
                    a[nxt]=min(a[now],ed[e].cap-ed[e].flow);
                    p[nxt]=e;
                    if(!inq[nxt])q.push(nxt),inq[nxt]=1;
                }
            }
        }
        return a[t];
    }
    pll maxflow_mincost(int s,int t){
        ll flow=0,cost=0;
        while(spfa(s,t)){
            flow+=a[t];
            for(int now=t;now!=s;now=ed[p[now]^1].to){
                ed[p[now]].flow+=a[t];
                ed[p[now]^1].flow-=a[t];
                cost+=ed[p[now]].val*a[t];
            }
        }
        return {flow,cost};
    }
};

Dinic模板

通过 \(dis\) 判断是否为最短增广路,每次只在最短增广路上增广。

struct SPP_DC{
    int n,cnt;
    int hd[N],fr[N];
    struct edge{
        int ne,to;
        ll cap,flow,val;
    }ed[M*2];
    ll dis[N];int dp[N];
    queue<int> q;bool inq[N];

    void init(int num){
        n=num;
        cnt=0;
        rep(i,0,n)hd[i]=-1;
    }
    void adde(int a,int b,ll cap,ll val){
        ed[cnt].ne=hd[a];hd[a]=cnt;
        ed[cnt].to=b;
        ed[cnt].cap=cap;
        ed[cnt].flow=0;
        ed[cnt].val=val;
        cnt++;
        ed[cnt].ne=hd[b];hd[b]=cnt;
        ed[cnt].to=a;
        ed[cnt].cap=0;
        ed[cnt].flow=0;
        ed[cnt].val=-val;
        cnt++;
    }
    bool spfa(int s,int t){
        rep(i,0,n)inq[i]=0,dis[i]=INF;
        dis[s]=0;
        q.push(s);
        inq[s]=1;
        while(!q.empty()){
            int now=q.front();q.pop();
            inq[now]=0;
            for(int e=hd[now];~e;e=ed[e].ne){
                int nxt=ed[e].to;
                if(ed[e].cap>ed[e].flow&&dis[nxt]>dis[now]+ed[e].val){
                    dis[nxt]=dis[now]+ed[e].val;
                    dp[nxt]=dp[now]+1;
                    if(!inq[nxt])q.push(nxt),inq[nxt]=1;
                }
            }
        }
        return dis[t]!=INF;
    }
    pll dfs(int now,int t,ll flow){
        if(now==t||!flow)return {flow,0};
        pll res={0,0};
        for(int &e=fr[now];~e;e=ed[e].ne){
            int nxt=ed[e].to;
            if(ed[e].cap>ed[e].flow&&dis[nxt]==dis[now]+ed[e].val&&dp[nxt]==dp[now]+1){
                ll f,c;
                tie(f,c)=dfs(nxt,t,min(flow-res.fir,ed[e].cap-ed[e].flow));
                res.fir+=f;res.sec+=c;
                ed[e].flow+=f;
                ed[e^1].flow-=f;
                res.sec+=ed[e].val*f;
                if(res.fir==flow)break;
            }
        }
        return res;
    }
    pll maxflow_mincost(int s,int t){
        ll flow=0,cost=0;
        while(spfa(s,t)){
            rep(i,0,n)fr[i]=hd[i];
            auto res=dfs(s,t,INF);
            flow+=res.fir;cost+=res.sec;
        }
        return {flow,cost};
    }
};

上下界

无源汇

可行流

判断是否存在可行流量。

使用求最大流解决,我们考虑消去下界,这样问题就转化成了普通的网络流问题(还缺源点和汇点)。

具体的,对于每个点,上下界同时减去下界即可。

这时候我们发现流量不守恒了,计算出当前节点流量守恒需要的流量 \(d_i=(流入的下界之和-流出的下界之和\)),考虑构建一个附加源点和附加汇点,若 \(d_i<0\),则向汇点连一条 \(-d_i\) 容量的边,否则从源点连一条 \(d_i\) 容量的边,从而消去多出的出流量或入流量。

最后对附加源点到附加汇点跑一遍最大流即可,若所有附加边全部满流(判断附加源点的出边即可),则存在可行流。

此时每一条边的流量就是求出的流量加上它的下界。

有源汇

可行流

我们可以把这个问题转化成无源汇可行流问题。

具体的,连一条 \(t\rightarrow s\) 的上界为 \(\infty\) 下界为 \(0\) 的边,然后整个图就无源汇了。

若有解,那么可行流流量就是上面那条附加流的流量。

最大流

我们先跑一遍可行流。

这时候我们只需要榨干获取整个网络的剩余流量即可。

先删除所有附加边(事实上,只需要删除那个连接源点与汇点的边即可,因为别的附加边都满流了)。

然后在残量网络里用给出的源点和汇点再跑一遍最大流,二者相加即可。

最小流

类似地,我们退掉所有没有的流量就行。

更具体地,残量网络里跑一遍 \(t\rightarrow s\) 的最大流,可行流答案减去多余最大流就是最小流。

费用流

没有区别,把上面所有部分跑最大流的算法改成对应的费用流算法就行了。

模板(Dinic)

不推荐额外封装,直接在外部模拟,用上面的板子就行。

下面给出一个非费用流的封装示例。

struct BoundDinic{
    int n,cnt;
    int hd[N],fr[N];
    struct edge{
        int ne,to;
        ll low,cap,flow;
    }ed[M*2+N*2*2+2];
    int dp[N];
    queue<int> q;

    void init(int num){
        n=num;
        cnt=0;
        rep(i,0,n)hd[i]=-1;
    }
    void adde(int a,int b,ll low,ll cap){
        ed[cnt].ne=hd[a];hd[a]=cnt;
        ed[cnt].to=b;
        ed[cnt].low=low;ed[cnt].cap=cap;
        ed[cnt].flow=0;
        cnt++;
        ed[cnt].ne=hd[b];hd[b]=cnt;
        ed[cnt].to=a;
        ed[cnt].low=0;ed[cnt].cap=0;
        ed[cnt].flow=0;
        cnt++;
    }
    int bfs(int s,int t){
        rep(i,0,n)dp[i]=0;
        dp[s]=1;
        q.push(s);
        while(!q.empty()){
            int now=q.front();q.pop();
            for(int e=hd[now];~e;e=ed[e].ne){
                int nxt=ed[e].to;
                if(ed[e].cap>ed[e].flow&&!dp[nxt]){
                    dp[nxt]=dp[now]+1;
                    q.push(nxt);
                }
            }
        }
        return dp[t];
    }
    ll dfs(int now,int t,ll flow){
        if(now==t||!flow)return flow;
        ll res=0;
        for(int &e=fr[now];~e;e=ed[e].ne){
            int nxt=ed[e].to;
            if(ed[e].cap>ed[e].flow&&dp[nxt]==dp[now]+1){
                ll f=dfs(nxt,t,min(flow-res,ed[e].cap-ed[e].flow));
                res+=f;
                ed[e].flow+=f;
                ed[e^1].flow-=f;
                if(res==flow)break;
            }
        }
        return res;
    }
    ll maxflow(int s,int t){
        ll res=0;
        while(bfs(s,t)){
            rep(i,0,n)fr[i]=hd[i];
            res+=dfs(s,t,INF);
        }
        return res;
    }

    ll d[N];
    void LoopInit(int &s,int &t){
        s=n+1,t=n+2;
        hd[s]=hd[t]=-1;
        rep(i,0,cnt-1){
            if(i&1)d[ed[i].to]-=ed[i^1].low;
            else d[ed[i].to]+=ed[i].low;
            ed[i].cap-=ed[i].low;
        }
        rep(i,0,n){
            if(d[i]>0)adde(s,i,0,d[i]);
            if(d[i]<0)adde(i,t,0,-d[i]);
        }
        n+=2;
    }
    bool LoopPossibleFlow(){
        int s,t;
        LoopInit(s,t);
        maxflow(s,t);
        for(int e=hd[s];~e;e=ed[e].ne){
            if(ed[e].flow<ed[e].cap)return 0;
        }
        return 1;
    }

    int loop;
    ll RootPossibleFlow(int s,int t){
        loop=cnt;
        adde(t,s,0,INF);
        if(!LoopPossibleFlow())return -1;
        else return ed[loop].flow+ed[loop].low;
    }
    ll RootMaxFlow(int s,int t){
        ll res=RootPossibleFlow(s,t);
        if(res==-1)return -1;
        ed[loop].cap=ed[loop].flow=0;
        ed[loop^1].cap=ed[loop^1].flow=0;
        return res+maxflow(s,t);
    }
    ll RootMinFlow(int s,int t){
        ll res=RootPossibleFlow(s,t);
        if(res==-1)return -1;
        ed[loop].cap=ed[loop].flow=0;
        ed[loop^1].cap=ed[loop^1].flow=0;
        return res-maxflow(t,s);
    }
};
posted @   LastKismet  阅读(36)  评论(2编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示