网络流学习笔记
注:笔者是蒟蒻,所以本文几乎是干货,枯燥无味甚至可能会引人不适,请读者谨慎阅读。
为了笔者快爆掉的肝点个赞好吗???
Part.1 网络流基础定义
一个有向带权图
- 图中只有一个入度为
的点,即源点 ; - 图中只有一个出度为
的点,即汇点 ; - 每条边
的权值为正数,这个数表示此边的容量,记作 。
网络流可以具象为流水的系统,源点就是水源,能无限的供应水,而汇点就是取水点,无限的消耗水,边就是管道,而且通过水的数量不能超过其容量。
形式化的讲,记在边
; ;- 对于一个点
,和所有 的 以及 的 ,有 ,这个性质被称作流量守恒(即流入多少水就流出多少水)。
增广路定义为一条从
残量网络定义为已经流过一些流量后删除满流的边的网络。
Part.2 最大流及其应用
模板题。
定义最大流为在满足上述性质的前提下,流入汇点的流量总和的最大值。重点在于如何求这个值。
首先有一个贪心,就是每次选择一个增广路,流出路径上的剩余流量的最小值,然后更新剩余流量。
上述算法显然是错误的,那我们如何补救呢?
考虑对于每个边建立反向边,初始的容量为
可以发现,建立反向边后,相当于给了一次撤销的机会,而上述假贪心加上这个优化过后就是对的了,笔者并不会证明。这个算法就是 FF 算法,时间复杂度与流量有关。
而每次选择源点到汇点的最短增广路进行增广,就得到了 EK 算法,时间复杂度
每次只增广一条太亏了,所以把所有最短路径一起增广。具体的,以
注意到有效边组成的图一定是个 DAG,说明如果将一条边的剩余容量变成
这个小优化叫做当前弧优化,加上这个优化的算法叫做 Dinic。其时间复杂度就降到了
放一下板子的代码:
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e2+5,M = 5e3+5;
int n,m,s,t,cnt = 1,head[N],to[M<<1],nxt[M<<1],g[M<<1];//cnt初始值为1方便获得反边
inline void add(int x,int y,int z)
{
nxt[++cnt] = head[x];
head[x] = cnt;
to[cnt] = y,g[cnt] = z;
}
int dis[N],now[N];
inline bool bfs()
{
for(int i = 1;i<=n;i++) dis[i] = -1;
queue<int> q;
q.push(s);
dis[s] = 0;
now[s] = head[s];
while(!q.empty())
{
int u = q.front();q.pop();
for(int i = head[u];i;i = nxt[i])
{
int v = to[i];
if(g[i]>0&&dis[v]==-1)
{
q.push(v);
dis[v] = dis[u]+1,now[v] = head[v];
if(v==t) return 1;
}
}
}
return 0;
}
int dfs(int u,int s)
{
if(u==t) return s;
int k,res = 0;
for(int i = now[u];i;i = nxt[i])
{
if(!s) break;
int v = to[i];
now[u] = i;//当前弧优化
if(g[i]>0&&dis[v]==dis[u]+1)//能流且位于下一层
{
k = dfs(v,min(s,g[i]));
if(k==0) dis[v] = 2e9;
g[i]-=k,g[i^1]+=k,res+=k,s-=k;
}
}
return res;
}
signed main()
{
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>n>>m>>s>>t;
for(int i = 1,u,v,w;i<=m;i++)
cin>>u>>v>>w,add(u,v,w),add(v,u,0);
int ans = 0;
while(bfs()) ans+=dfs(s,2e9);
cout<<ans;
return 0;
}
最大流的用处太多了,后文慢慢讲吧。
Part.3 费用流及其应用
模板题。
费用流其实就是在最大流上对于每个弧增加了一个属性:费用,每流过一个单位的流就会消耗对应的费用。让你求在流最大的情况下使得费用最少或最大。
先想想费用流中的反向边怎么建,其实就是正向边的费用和反向边的费用成相反数就行了。
那怎么增广呢?还记得最大流中的 EK 算法吗,这个算法在求最大流是不是寻找的最短的增广路吗,其实改到费用流里就每次把费用最少的增广路拿出来增广就行了(这里是求最小费用最大流)。然而由于图有负边权,所以只能跑 SPFA。
时间复杂度是
放代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 5e3+5,M = 1e5+5;
int n,m,s,t,cnt = 1,head[N],now[N],nxt[M],to[M],g[M],fl[M];
inline void add(int x,int y,int w,int flow)
{
nxt[++cnt] = head[x];
head[x] = cnt;
to[cnt] = y,g[cnt] = w,fl[cnt] = flow;
nxt[++cnt] = head[y];
head[y] = cnt;
to[cnt] = x,g[cnt] = -w,fl[cnt] = 0;
}
int dis[N],mn[N];
bool vis[N];
inline bool spfa()
{
for(int i = 1;i<=n;i++)
dis[i] = 2e9,vis[i] = 0;
dis[s] = 0,mn[s] = 2e9,vis[s] = 1;
queue<int> q;
q.push(s);
while(!q.empty())
{
int u = q.front();q.pop();
vis[u] = 0;
for(int i = head[u];i;i = nxt[i])
{
int v = to[i],w = g[i];
if(fl[i]&&dis[v]>dis[u]+w)
{
now[v] = i,dis[v] = dis[u]+w,mn[v] = min(mn[u],fl[i]);
if(!vis[v]) vis[v] = 1,q.push(v);
}
}
}
if(dis[t]!=2e9) return 1;
return 0;
}
signed main()
{
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>n>>m>>s>>t;
for(int i = 1,u,v,w,flow;i<=m;i++)
cin>>u>>v>>flow>>w,add(u,v,w,flow);
int ans1 = 0,ans2 = 0;
while(spfa())
{
ans1+=mn[t],ans2+=mn[t]*dis[t];
int x = t;
while(x!=s)
{
fl[now[x]]-=mn[t],fl[now[x]^1]+=mn[t];
x = to[now[x]^1];
}
}
cout<<ans1<<' '<<ans2<<'\n';
return 0;
}
至于最大费用最大流的求法,你可以更改一下 SPFA 或者将费用取反写最小费用最大流都可以。
还是来几道例题吧(可能比较多,读者可以选择性阅读)。
例题一:P2045。
考虑将每个点
- 一条
,费用为 ,容量为 的边(只能取一次),表示将 中的数取出来; - 一条
,费用为 ,容量为 的边,表示直接跳过 ; - 一条
或者 ,费用为 ,容量为 的边,表示往下或者往右走。
跑最大费用最大流即可。
例题二:P3358。
首先发现一个开区间对其内部的点都有相同的覆盖次数贡献,所以可以对区间的两个端点离散化。
首先可以想到将数轴上的点用费用为
设流入当前点的流量为
建立超级源点和超级汇点跑最大费用最大流即可。
例题三:CF277E。
发现一个点选父亲和这个点选儿子是互相独立的,所以考虑拆点,
建立超级源点
如果两个点
跑最大费用最大流即可,值得注意的是如果最终的最大流不是
例题四:CF863F。
首先可以暴力预处理出每个点的最终范围
注意到
最后的建图就十分显然了,建立超级源点
然后跑最小费用最大流即可。
放一道不错的练习题:P4249以及其双倍经验CF1264E。
Part.4 最小割及其应用
定义一张图的割为一种点的划分方式:将图分为
最小割的几个性质:
- 最小割和最大流是相等的,即最大流最小割定理;
- 最小割的割边一定是满流边;
- 若删掉或减小某条满流边的容量后最大流不减小,则该边一定不是最小割中的边;
- 网络流的任意一条增广路至少经过一条最小割边;
- 对于某满流边,如果在残留网络中,源点能到达该边一端点,另一端点能到达汇点,则该满流边就是关键割边(即一定是最小割的一个割边)。
例题一:最大权闭合子图。
给出一张有向图
,每个点有一个权值 。现在要选出一个点集 ,满足对于任意一条边 ,如果 ,则 。你需要让 最大。
首先考虑最理想的情况,即所有
设在一组割中与
对于在原图中的一条边
跑出来的最大流就是最小的代价,这样答案就能被轻松求出。
例题二:P2057。
考虑用最小割求解,在一组割中,与
首先如果
对于一个朋友关系
跑最小割即可。
练习题:P4313。
Part.5 上下界网络流及其应用
首先什么是上下界网络流,就是每条边除了流出网络的上限
首先是无源汇上下界可行流。
什么是无源汇,相当于网络中的每个点都满足流量守恒。让你求出满足这些条件的一种流法。
首先先考虑去掉下界的限制,即每条边先流出
设
考虑建图求解,对于
加上费用也很简单,用
那如何求有源汇上下界可行流/最大流/最小流呢?
第一个直接转化为无源汇上下界可行流,连一条
那如何求最大流呢?
考虑调整法。还记得求可行流时建的图吗,其实我们只需要拆掉
同样的去求最小流,发现跑
这里放一下求最大流的模板的代码:
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e2+5,M = 1e4+N;
int n,m,s,t,ss,tt,a[N],cnt = 1,head[N],to[M<<1],nxt[M<<1],g[M<<1];
inline void add(int x,int y,int z)
{
nxt[++cnt] = head[x];
head[x] = cnt;
to[cnt] = y,g[cnt] = z;
nxt[++cnt] = head[y];
head[y] = cnt;
to[cnt] = x,g[cnt] = 0;
}
int dis[N],now[N];
inline bool bfs()
{
for(int i = 0;i<=n+1;i++) dis[i] = -1;
queue<int> q;
q.push(s);
dis[s] = 0;
now[s] = head[s];
while(!q.empty())
{
int u = q.front();q.pop();
for(int i = head[u];i;i = nxt[i])
{
int v = to[i];
if(g[i]>0&&dis[v]==-1)
{
q.push(v);
dis[v] = dis[u]+1,now[v] = head[v];
if(v==t) return 1;
}
}
}
return 0;
}
int dfs(int u,int s)
{
if(u==t) return s;
int k,res = 0;
for(int i = now[u];i;i = nxt[i])
{
if(!s) break;
int v = to[i];
now[u] = i;
if(g[i]>0&&dis[v]==dis[u]+1)
{
k = dfs(v,min(s,g[i]));
if(k==0) dis[v] = 2e9;
g[i]-=k,g[i^1]+=k,res+=k,s-=k;
}
}
return res;
}
inline int dinic()
{
int res = 0;
while(bfs()) res+=dfs(s,2e18);
return res;
}
signed main()
{
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>n>>m>>ss>>tt;
s = 0,t = n+1;
for(int i = 1,u,v,c,b;i<=m;i++)
{
cin>>u>>v>>b>>c;
add(u,v,c-b);
a[u]-=b,a[v]+=b;
}
int mx = 0;
for(int i = 1;i<=n;i++)
if(a[i]>0) add(s,i,a[i]),mx+=a[i];
else if(a[i]<0) add(i,t,-a[i]);
add(tt,ss,1e18);
if(dinic()<mx) return cout<<"please go home to sleep",0;
mx = g[cnt];
g[cnt] = g[cnt-1] = 0;
s = ss,t = tt;
cout<<mx+dinic();
return 0;
}
例题一:P4843。
建立超级源点
跑有源汇上下界最小流即可。
例题二:P5192。
对于每天和每个少女飞别建一个点,建超级源点
跑有源汇上下界最大流即可。
Part.6 二分图
一张图是二分图当且仅当这张图可以分为两个不相交的点集
一个图是二分图的充分必要条件是这张图里面没有奇环。当然也可以用黑白染色来判断一个图是否是二分图。
二分图的边覆盖/匹配/点覆盖/独立集问题
对于一张图,有如下定义:
- 边覆盖:
的一个边集 满足其是 的边覆盖,当且仅当对于每个点 ,存在另一个点 使得 ,即每个点至少是 中一条边的端点; - 匹配:
的一个边集 满足其是 的匹配,当且仅当 中任意两条边不存在共同点; - 点覆盖:
的一个点集 是 的点覆盖当且仅当对于图中每条边,其端点至少有一个属于 ; - 独立集:
的一个点集 是 的点覆盖当且仅当对于图中每条边,其端点至多有一个属于 ;
事实上,对于任意一张图
- 如果
存在边覆盖,则最小边覆盖加上最大匹配等于 。
证明:首先构造出一个最大匹配 ,记其端点组成的集合大小为 ,明显 ,现在考虑加边是其变为最小边覆盖,而每加一条边 会加一,最后我们需要将 变为 ,所以加边个数为 ,最小边覆盖为 ,得证。 的最大独立集加上最小点覆盖大小刚好为 。
证明:对于 的最大独立集 ,显然每条边最多只有一个端点属于 。令 ,则每条边至少一个端点属于 ,这刚好与点覆盖的定义相同,即 就是这张图的最小点覆盖。
二分图最大匹配模板。
建立超级源点
二分图有一个性质就是最大匹配等于最小点覆盖,虽然笔者不会证明,但是这意味着求出了二分图最大匹配就相当于求出了另外三个的值。
接下来就是例题。
例题一:P4251。
这种每行选一个使得没有重复的列的题都有一个套路:将行和列连边,然后跑二分图最大匹配。
首先想到二分答案,我们需要判断答案是否小于等于
例题二:P2764。
首先当每个点自成一条路径的时候,当前的路径条数为
将每个点拆分为两个点
现在的问题在于如何输出方案。不难发现在图中流往哪个边流相当于这条边两个端点在同一条链里面,这样记录方案也非常简单了。
练习题:CF1592F2。
Hall 定理
注:此知识点几乎和网络流没有关系,仅为对二分图的补充,读者可以选择性阅读。
Hall 定理:对于一张二分图
练习题:CF338E。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 上周热点回顾(3.3-3.9)
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」