网络流学习笔记
网络流学习笔记
由于本人太菜了,至今不会网络流,写了篇很菜的学习笔记 qwq。
一些概念
网络:网络是指一种特殊的有向图
这里,我们记源点
可以想象一下,将有向图想象成一个庞大的水管系统,从一个端点倒水,水会从另外一个端点流出。
如图就是一张网络。(图丑勿喷)
流:对于一个网络
1. 对于每条边,流经该边的流量不得超过该边的容量:
2. 除了
这里同样可以想象,将水从源点倒入,显然只有汇点可以有水流出,其他的点都必须将流入的水全部排出到其他节点,这就是流守恒性。同时,水管中流的水不可以比容量大,这就是容量限制。
割:对于一个网络
形象地说,就是将网络中的一些水管直接切断,使得源点和汇点不再连通。切断的水管的容量之和就是割的容量。
以上的定义主要来自 oi-wiki,加上个人的理解。
常见问题
最大流问题:求从源点
最小割问题:找到一个割
费用流问题:每条边
最大流
祭出洛谷模板题中的样例:
其中,
不难算出,该网络中的最大流为
,该路线可通过 的流量。 ,可通过 的流量。 ,可通过 的流量(边 之前已经耗费了 的流量)。
计算得
不用上面自己的图是因为太难算了自己没算出来。
Edmonds-Karp 算法 (EK 算法)
在明白 EK 算法的实现过程之前,需要先了解:
增广路:在残留网络中,从源点
简单来说,就是还没有被榨干的水管 /doge。
反向边:对于每条边
反向边的作用是:允许我们撤销之前的流量选择,从而找到更优的流量分配方案,具体在后面讲 EK 算法还会再说。
EK 算法是一种基于增广路的求解最大流的算法。它的核心思想是:每次寻找从源点到汇点的最短增广路,然后沿着增广路更新流量,直到找不到增广路为止。
算法步骤:
- 寻找增广路:使用 BFS 寻找从源点到汇点的最短增广路。
- 更新流量:沿着增广路更新流量,正向边减去流量,反向边加上流量。
- 重复步骤 1 和 2:直到找不到增广路为止。
时间复杂度:
实现代码
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 算法更高效的最大流算法。通过分层图和多路增广提高效率。
算法步骤:
- 构造分层图:使用 BFS 构造从源点到各个顶点的分层图,记录每个顶点的层数。
- 多路增广:从源点开始,沿着分层图进行多路增广,每次尽可能地增加流量。
- 重复步骤 1 和 2:直到无法增广为止。
时间复杂度:
当前弧优化:在每次增广时,记录每个顶点已经访问过的边,下次增广时从上次访问的边开始继续访问,避免重复访问已经访问过的边。
实现代码
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&∑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 就行了呢?
而前文提到求最大流一个很重要的步骤就是建立反向边反悔,而一条正向边的费用显然大于
于是这个网络出现了负边,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
模型:二元关系最小割模型
题目大意
有一个
而一对好朋友如果能同时选文科或者理科,那么他们又将收获一些喜悦值。
求最大喜悦总和。
解题思路
考虑每个同学必须选择文科/理科(废话),容易想到对这个矩阵建网络求最小割。
先建立一个源点
对于第一种关系:同学选文科 or 理科的喜悦值,分别将该同学节点向
对于第二种关系:两个相邻的同学同选文科 or 理科的喜悦值,直接将这两个同学之间建边。
边权要怎么定?
参照这张图,我们解方程。
首先令第
第
然后对于每个点对
理解一下,这些边的贡献等于割掉这些边后会失去的贡献。
解方程:
由于方程的解需要对称,注意到当
根据这个方程建边,跑 Dinic 求出最小割,最后用总和减去最小割容量就是答案。
P4043 [AHOI2014/JSOI2014] 支线剧情
模型:有源汇有上下界最小费用流
题目大意
一个点游戏中从起始剧情点
每条边(支线剧情都有一个时间花费)都代表了一段剧情过渡。
由于游戏剧情不可逆,且无法回退,只能退出后重新从1开始,所以需要规划若干次游戏过程(路径),使得最终能够观看到所有的支线剧情,同时使得所有路径的总时间最小。
解题思路
将原问题转化为一个上下界的最小费用流问题。
由于所有边都至少要遍历一遍,所以先固定地把这条边计入答案,同时维护顶点的度数变化。
对于每条原始边,从
为了保证所有点都能连通源点,额外添加从所有点到
同时对于每个点:
- 如果
大于 (流入较多),从源点 到 加边; - 如果
小于 (流出较多),从 到汇点 加边。
为了使流环路收敛,构造一条从
EK + SPFA 求出最小总费用,将这个最小费用加到先计算的 ans
上就是最终的最小总时间。
P2766 最长不下降子序列问题
题目来源:网络流与线性规划24题
模型:最多不相交路径
题目大意
- 求最长的不下降子序列的长度
(子序列中元素按照原序列的顺序且允许相等)。 - 在每个元素只能使用一次的条件下,求最多可以选出多少个长度为
的不下降子序列(同一元素最多出现在一个子序列中)。 - 允许序列中的首元素
和末元素 可重复使用(其他元素仍只能用一次)的情况下,求最多能取出多少个不同的长度为 的不下降子序列,其中两个子序列的下标序列不同即认为不同。
解题思路
第一问是一个简单 dp,计算求解完之后将求得的答案和 f
数组保留,后面两问要用到。
第二问:
注意到
开始建图:定义源点
将每个点拆成两个点,
如果
如果
跑一边最大流就是答案。
第三问:
和第二问类似,只需要把 INF
即可。
最后还需要判断一下一种特殊情况:
如果序列严格递减,对于每个
这个时候由于我们的建图策略,INF
的边,INF
的边,INF
的边。
此时最大流为 INF
(雾。
但是我们知道,答案应该是
看完这些例题,像我这样的萌新肯定会产生疑惑:这些鬼魅的建图为什么都是对的?
这个时候建议手模一下这个建图,自己算一下样例,你就会发现其精妙所在。
虽然有可能证明不出来,但是差不多也能理解其正确性了。
那像我这样的萌新还是会产生疑惑:这些绝妙思路怎么才能自己想出来呢?
建议大家可以再看一下 这篇博客。
里面总结了各种网络流的建图技巧和模型以及分别的例题,属于史诗级巨作。
聪明的你,如果可以把里面每一个模型都理解透彻,那你就可以成为网络流大神了!
后面我可能还会再做一些不同模型的例题qwq。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】