网络流学习笔记

网络流学习笔记

由于本人太菜了,至今不会网络流,写了篇很菜的学习笔记 qwq。

更好的食用效果

一些概念

网络网络是指一种特殊的有向图 G=(V,E),存在容量和源汇点。

这里,我们记源点 s,汇点 t,边 [u,v] 的容量为 c(u,v)

可以想象一下,将有向图想象成一个庞大的水管系统,从一个端点倒水,水会从另外一个端点流出。

如图就是一张网络。(图丑勿喷)

对于一个网络 G=(V,E),流是一个从边集 E 的整数集或实数集的函数(记流函数 f(u,v)),满足:

1. 对于每条边,流经该边的流量不得超过该边的容量:0f(u,v)c(u,v)

2. 除了 s,t 之外,任意节点 u 的净流量为 0

这里同样可以想象,将水从源点倒入,显然只有汇点可以有水流出,其他的点都必须将流入的水全部排出到其他节点,这就是流守恒性。同时,水管中流的水不可以比容量大,这就是容量限制

对于一个网络 G=(V,E),割是指将点集 V 分为两个集合 ST,其中源点 sS,汇点 tT割的容量定义为所有从 ST 的边的容量之和。

形象地说,就是将网络中的一些水管直接切断,使得源点和汇点不再连通。切断的水管的容量之和就是割的容量。

以上的定义主要来自 oi-wiki,加上个人的理解。

常见问题

最大流问题求从源点 S 到汇点 T 的最大流量。

最小割问题找到一个割 (S,T),使得割的容量最小。

费用流问题每条边 (u,v) 除了有容量 c(u,v) 之外,还有一个费用 cost(u,v),表示单位流量流经该边所需要的费用。在保证最大流的前提下,求最小(或最大)的总费用。

最大流

祭出洛谷模板题中的样例:

其中,s=4t=3

不难算出,该网络中的最大流为 50

  • 423,该路线可通过 20 的流量。
  • 43,可通过 20 的流量。
  • 4213,可通过 10 的流量(边 42 之前已经耗费了 20 的流量)。

计算得 20+20+10=50

不用上面自己的图是因为太难算了自己没算出来。

Edmonds-Karp 算法 (EK 算法)

在明白 EK 算法的实现过程之前,需要先了解:

增广路在残留网络中,从源点 s 到汇点 t 的一条路径,其中路径上的所有边的剩余容量都大于 0

简单来说,就是还没有被榨干的水管 /doge。

反向边对于每条边 (u,v),我们都建立一条反向边 (v,u),容量为 0。当正向边 (u,v) 的流量增加时,反向边 (v,u) 的容量也增加,增加的值等于正向边增加的流量。

反向边的作用是:允许我们撤销之前的流量选择,从而找到更优的流量分配方案,具体在后面讲 EK 算法还会再说。

EK 算法是一种基于增广路的求解最大流的算法。它的核心思想是:每次寻找从源点到汇点的最短增广路,然后沿着增广路更新流量,直到找不到增广路为止。

算法步骤

  1. 寻找增广路:使用 BFS 寻找从源点到汇点的最短增广路。
  2. 更新流量:沿着增广路更新流量,正向边减去流量,反向边加上流量。
  3. 重复步骤 1 和 2:直到找不到增广路为止。

时间复杂度O(V×E2),其中 V 是顶点数,E 是边数。

实现代码

namespace EK{ //Edmonds-Karp算法求最大流
    vector<int> pre;
    vector<ll> dis;
    
    bool BFS(){
        vector<bool> vis(n+1,0);
        queue<int> q;

        q.push(s),vis[s]=1,dis[s]=INF;
        while(!q.empty()){
            int u=q.front();q.pop();

            for(auto v:G[u]){
                if(val_G[u][v]==0) continue; //找增广路只需要找残余网络中剩余容量大于0的边
                if(vis[v]) continue; //如果结点已经访问过,就不再访问

                dis[v]=min(dis[u],val_G[u][v]); //更新到达结点v的流量(求最小值)
                pre[v]=u; //记录结点v的前驱结点,方便修改边权
                q.push(v),vis[v]=1;
                if(v==t) return 1; //如果可以从源点到达汇点,说明还存在增广路 
            }
        }

        return 0;//无法从源点到达汇点,说明不存在增广路
    }

    ll solve(){
        dis.resize(n+1,0);
        pre.resize(n+1,0);
        
        ll res=0;
        while(BFS()){
            int x=t;
            while(x!=s){
                int v=pre[x]; //找到结点x的前驱结点
                val_G[v][x]-=dis[t]; //正向边减去流量
                val_G[x][v]+=dis[t]; //反向边加上流量
                x=v;
            }
            res+=dis[t];
        }
        return res;
    }
}

Dinic 算法

显然,EK 算法的时间复杂度是不够优秀的,每次都有可能遍历整个残量网络。

Dinic 算法是一种比 EK 算法更高效的最大流算法。通过分层图和多路增广提高效率。

算法步骤

  1. 构造分层图:使用 BFS 构造从源点到各个顶点的分层图,记录每个顶点的层数。
  2. 多路增广:从源点开始,沿着分层图进行多路增广,每次尽可能地增加流量。
  3. 重复步骤 1 和 2:直到无法增广为止。

时间复杂度O(V2E),对于某些特殊图可以达到 O(V2E)

当前弧优化:在每次增广时,记录每个顶点已经访问过的边,下次增广时从上次访问的边开始继续访问,避免重复访问已经访问过的边。

实现代码

namespace Dinic{ //Dinic算法求最大流
    vector<ll> pre,now,dis;

    bool BFS(){ 
        dis.assign(n+5,INF);
        now.assign(n+5,0);

        queue<int> q;
        q.push(s),dis[s]=0;
        now[s]=head[s];//当前弧优化

        while(!q.empty()){ //BFS找到层次网络
            int x=q.front();q.pop(); 

            for(int i=head[x];i;i=e[i].nxt){
                int v=e[i].to;
                if(e[i].val<=0||dis[v]!=INF) continue;

                dis[v]=dis[x]+1; //更新层次
                now[v]=head[v]; //更新当前弧
                q.push(v);

                if(v==t) return 1;
            }
        }
        return 0;
    }

    ll DFS(int u,ll sum){ //DFS找到增广路
        if(u==t)return sum;

        ll k,res=0;
        for(int i=now[u];i&&sum;i=e[i].nxt){
            now[u]=i;
            int v=e[i].to;
            if(e[i].val<=0||(dis[v]!=dis[u]+1))continue;

            k=DFS(v,min(sum,e[i].val)); //找到增广路上的最小流量
            if(k==0)dis[v]=INF;

            e[i].val-=k,e[i^1].val+=k; //正向边减去流量,反向边加上流量
            res+=k,sum-=k;
        }

        return res;
    }
    
    ll solve(){
        pre.assign(n+5,0);
        now.assign(n+5,0);

        ll res=0;
        while(BFS()){ //每次找到一条增广路,就更新一次层次网络
            res+=DFS(s,INF); //每次找到一条增广路,就更新最大流
        }
        return res;
    }
}

最小割

最大流最小割定理: 对于任意网络,最大流的值等于最小割的容量。

证明可以参照 oi-wiki

根据定理,求出一个网络的最大流就可以求出该网络的最小割的容量。

费用流

在求最大流的同时添加了每条边的费用,使费用最小。

EK+SPFA 求费用流

EK 求最大流的过程就是通过 BFS 不断寻找增广路,每次找到一条之后更新,通过反向边进行反悔。

现在加入了费用的概念,不难联想到最短路算法。

此时会有一个大胆的想法:SPFA 和 Dij 都是基于 BFS 进行的,那么是不是只需要把 EK 中的 BFS 替换成 SPFA/Dij 就行了呢?

而前文提到求最大流一个很重要的步骤就是建立反向边反悔,而一条正向边的费用显然大于 0,而反向边为了能够正确地进行反悔,其费用就需要取正向边的相反数

于是这个网络出现了负边,Dij 倒闭了。

那为什么把 BFS 换成 SPFA 求最小费用最大流就是对的呢?

  • 如果两条边的流量相同,我们需要找到费用较小的那一条边,这一部分 SPFA 显然是对的。

  • 如果流量较大的那条边费用较小,用最短路算法找增广路就一定会找到这条边,可以保证流量最大且费用最小。

  • 如果流量较大的那条边费用较大,用最短路算法第一遍会找到费用较小的那条边,于是流量大的边成为了图中的一条增广路。按照 EK 的求解步骤,流量较大的边还是会被找到并更新。

因此就可以用 EK+SPFA 求最小费用最大流了!

实现代码

namespace EK{ //EK+SPFA求最大流最小费用
    vector<ll> dis,F;
    vector<int> pre,vis;

    bool SPFA(){
        dis.assign(n+1,INF),F.assign(n+1,INF);
        pre.assign(n+1,0),vis.assign(n+1,0);

        queue<int> q;
        q.push(s);

        dis[s]=0,vis[s]=1,F[s]=INF;
        while(q.size()){
            int u=q.front();
            q.pop();
            vis[u]=0;

            for(int i=head[u];i;i=e[i].nxt){
                int v=e[i].to;

                if(e[i].w&&dis[v]>dis[u]+e[i].c){
                    dis[v]=dis[u]+e[i].c;
                    F[v]=min(F[u],e[i].w);
                    pre[v]=i;
                    if(!vis[v]){
                        q.push(v);
                        vis[v]=1;
                    }
                }
            }
        }

        return dis[t]!=INF;
    }

    pair<ll,ll> solve(){
        ll max_flow=0,cost=0;
        while(SPFA()){
            for(int i=t;i!=s;i=e[pre[i]^1].to){
                e[pre[i]].w-=F[t];
                e[pre[i]^1].w+=F[t];
            }

            max_flow+=F[t];
            cost+=dis[t]*F[t];   
        }

        return {max_flow,cost};
    }   
}

网络流建图技巧例题

参考

P1646 [国家集训队] happiness

模型:二元关系最小割模型

题目大意

有一个 n×m 的同学矩阵,现在要分文理科,每个同学对于选择文科与理科有着自己的喜悦值。

而一对好朋友如果能同时选文科或者理科,那么他们又将收获一些喜悦值。

求最大喜悦总和。

解题思路

考虑每个同学必须选择文科/理科(废话),容易想到对这个矩阵建网络求最小割。

先建立一个源点 s 和一个汇点 t,每个同学都建一个点,共 n×m+2 个点。

对于第一种关系:同学选文科 or 理科的喜悦值,分别将该同学节点向 st 建边。

对于第二种关系:两个相邻的同学同选文科 or 理科的喜悦值,直接将这两个同学之间建边。

边权要怎么定?

参照这张图,我们解方程。

首先令第 i 位同学选文科的喜悦值为 Ai;第 i 位同学选理科的喜悦值为 Bi

ij 位同学同选文科喜悦值为 Ci,j;第 i 和第 j 位同学同选理科的喜悦值为 Di,j

然后对于每个点对 (i,j) 列出方程:(按照同文、同理、一文一理、一理一文的顺序)。

{c+d=Bi+Bi+Di,j    (1)a+b=Aj+Aj+Ci,j    (2)b+c+e=Bi+Aj+Ci,j+Di,j    (3)a+d+f=Ai+Bj+Ci,j+Di,j    (4)

理解一下,这些边的贡献等于割掉这些边后会失去的贡献

解方程:(3)+(4)(1)(2) 得到 e+f=Ci,j+Di,j

e=f,所以 e=f=Ci,j+Di,j2

由于方程的解需要对称,注意到当 a=Ai+ci,j2 时,解得:

{a=Ai+ci,j2,b=Aj+ci,j2c=Bi+di,j2,d=Bj+di,j2

根据这个方程建边,跑 Dinic 求出最小割,最后用总和减去最小割容量就是答案。

P4043 [AHOI2014/JSOI2014] 支线剧情

模型:有源汇有上下界最小费用流

题目大意

一个点游戏中从起始剧情点 1 开始,所有剧情点都是从 1 号可达,并且无法回到已经经过的剧情点。

每条边(支线剧情都有一个时间花费)都代表了一段剧情过渡。

由于游戏剧情不可逆,且无法回退,只能退出后重新从1开始,所以需要规划若干次游戏过程(路径),使得最终能够观看到所有的支线剧情,同时使得所有路径的总时间最小。

解题思路

将原问题转化为一个上下界的最小费用流问题。

由于所有边都至少要遍历一遍,所以先固定地把这条边计入答案,同时维护顶点的度数变化。

对于每条原始边,从 iv 加一条无限容量、费用 w 的边。

为了保证所有点都能连通源点,额外添加从所有点到 1 号(源点)的边(容量无限,费用 0)。

同时对于每个点:

  • 如果 di 大于 0(流入较多),从源点 si 加边;
  • 如果 di 小于 0(流出较多),从 i 到汇点 t 加边。

为了使流环路收敛,构造一条从 ts 的边(无限容量、费用 0),实际上是构成了一个流的循环结构。

EK + SPFA 求出最小总费用,将这个最小费用加到先计算的 ans 上就是最终的最小总时间。

P2766 最长不下降子序列问题

题目来源网络流与线性规划24题

模型:最多不相交路径

题目大意

  1. 求最长的不下降子序列的长度 s(子序列中元素按照原序列的顺序且允许相等)。
  2. 在每个元素只能使用一次的条件下,求最多可以选出多少个长度为 s 的不下降子序列(同一元素最多出现在一个子序列中)。
  3. 允许序列中的首元素 x1 和末元素 xn 可重复使用(其他元素仍只能用一次)的情况下,求最多能取出多少个不同的长度为 s 的不下降子序列,其中两个子序列的下标序列不同即认为不同。

解题思路

第一问是一个简单 dp,计算求解完之后将求得的答案和 f 数组保留,后面两问要用到。

第二问:

注意到 ajai 可以构成一个不下降子序列,当且仅当 jiajai

开始建图:定义源点 s=0,汇点 t=2×n+1

将每个点拆成两个点,ii+n 连边,容量为 1

如果 fi=1,则 si 连边,容量为 1

如果 fi=maxLIS,则 i+nt 连边,容量为 1

跑一边最大流就是答案。

第三问:

和第二问类似,只需要把 1n 相关的边的容量改为 INF 即可。

最后还需要判断一下一种特殊情况:

如果序列严格递减,对于每个 i 都有 fi=1,恰好 s=1

这个时候由于我们的建图策略,si 有一条 INF 的边,ii+n 有一条 INF 的边,i+nt 有一条 INF 的边。

此时最大流为 INF(雾。

但是我们知道,答案应该是 n,特判一下就好了。


看完这些例题,像我这样的萌新肯定会产生疑惑:这些鬼魅的建图为什么都是对的?

这个时候建议手模一下这个建图,自己算一下样例,你就会发现其精妙所在。

虽然有可能证明不出来,但是差不多也能理解其正确性了。

那像我这样的萌新还是会产生疑惑:这些绝妙思路怎么才能自己想出来呢?

建议大家可以再看一下 这篇博客

里面总结了各种网络流的建图技巧和模型以及分别的例题,属于史诗级巨作。

聪明的你,如果可以把里面每一个模型都理解透彻,那你就可以成为网络流大神了!

后面我可能还会再做一些不同模型的例题qwq。

posted @   SunburstFan1106  阅读(6)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示