网络流学习笔记
网络流学习笔记
前言:从 2022.12.23 到 2023.1.10,学了一年网络流(狗头,是时候总结一下了,当然之后肯定还会再刷网络流的。
upd 2024.7.4:修改了一些排版和规范。写的很烂,还没有补充修改一些知识,所以可能还是有点晦涩难懂。
目录#
1. 网络流
这边罗列一下网络的定义和流的相关性质。
网络:一张有向图
对于每条边
1.1 流#
每条边有流量
- 容量限制,
,也就是这条边经过的流量不能超过边本身的容量。 - 斜对称性:
, 的流量是 流量的相反数。 - 流守恒性:从这个点流进来的流量等于从这个点流出去的流量,从源点流出的流量等于汇点流入的流量,不会有流量凭空消失。
剩余容量:
2. 最大流/最小割
2.1 最大流#
2.1.1 问题#
给定一张网络,有源点和汇点,求从源点流到汇点的最大流量。
2.1.2 算法#
这边就要引入网络流算法了,有
2.1.3 原理#
这边我先讲了算法,大家可能会疑惑,为什么这样子找的路径一定是最大流的路径呢?如果只是单纯的找路径,肯定不能保证一定是正确的路径,所以这里用了一个方法,建反向边,这边举一个例子:
一张网络,源点
显然这张网络的最大流是
当我们这条边流过多少流量,原边减去流量,反边加上流量,如图:
我们可以发现,在当前包括反向边的网络中,还有一条增广路:
也就是这一条,我们经过
2.1.4 注意事项#
网络流一般点少边多,也就是稠密图中,
注意初始时,链式前向星的
2.1.5 代码#
bool bfs(){
queue<ll> q;
memset(pre, 0, sizeof(pre));
for(int i = 1; i <= n; i++) vis[i] = 0;
vis[s] = 1;
q.push(s);
while(!q.empty()){
int u = q.front();
q.pop();
for(int i = h[u]; i; i = e[i].nxt){
int v = e[i].to;
if(vis[v] || e[i].w == 0) continue;
vis[v] = 1;
pre[v].v = u;
pre[v].nxt = i;
if(v == t) return 1;
q.push(v);
}
}
return 0;
}
ll EK(){
ll ans = 0, minn = 0x3f3f3f3f;
while(bfs()){
minn = 0x3f3f3f3f;
for(int i = t; i != s; i = pre[i].v){ //遍历路径
minn = min(minn, e[pre[i].nxt].w);
}
for(int i = t; i != s; i = pre[i].v){
e[pre[i].nxt].w -= minn;
e[pre[i].nxt ^ 1].w += minn; //反向边
}
ans += minn;
}
return ans;
}
bool bfs(){
queue<int> q;
for(int i = 1; i <= n; i++) dis[i] = 0x3f3f3f3f;
dis[s] = 0;
q.push(s);
now[s] = h[s];
while(!q.empty()){
int u = q.front();
q.pop();
for(int i = h[u]; i; i = e[i].nxt){
int v = e[i].to;
if(e[i].w > 0 && dis[v] == 0x3f3f3f3f){
dis[v] = dis[u] + 1; //分层
q.push(v);
now[v] = h[v];
if(v == t) return 1;
}
}
}
return 0;
}
int dfs(int u, int sum){
if(u == t) return sum;
int minn, ans = 0;
for(int i = now[u]; i && sum; i = e[i].nxt){
now[u] = i; //当前弧优化
int v = e[i].to;
if(e[i].w && dis[v] == dis[u] + 1){
minn = dfs(v, min(e[i].w, sum));
if(minn == 0) dis[v] = 0x3f3f3f3f; //这条路径已经流不出去了
e[i].w -= minn;
e[i ^ 1].w += minn;
ans += minn;
sum -= minn;
}
}
return ans;
}
int dinic(){
int ans = 0;
while(bfs()){
ans += dfs(s, 0x3f3f3f3f);
}
return ans;
}
2.2 最小割#
割:将网络中的点划分成
2.2.1 问题#
给定一张网络,有源点和汇点,求用最少的边,覆盖从源点到汇点的所有路径,也就是求最小的割。
通俗一点,就是割掉最少的边,使得
最大流/最小割定理:可以证明,最大流
割所能表达的意义之后会讲,所以,跑一遍最大流就行了。
3. 最小费用最大流
3.1 问题#
给定一张网络,有源汇点,每条边有
在保证最大流的情况下,让费用最小。
我们只需要在最大流算法的基础上,把找增广路的
基于
bool bfs(){
queue<int> q;
for(int i = 1; i <= n; i++) dis[i] = 0x3f3f3f3f, vis[i] = 0;
vis[s] = 1;
dis[s] = 0;
q.push(s);
while(!q.empty()){
int u = q.front();
vis[u] = 0;
q.pop();
for(int i = h[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;
if(!vis[v]){
vis[v] = 1;
q.push(v);
}
}
}
}
return dis[t] != 0x3f3f3f3f;
}
int dfs(int u, int sum){
if(u == t) return sum;
int minn, ans = 0;
vis[u] = 1;
for(int i = now[u]; i && sum; i = e[i].nxt){
int v = e[i].to;
now[u] = i; //当前弧优化
if(e[i].w && dis[v] == dis[u] + e[i].c && !vis[v]){
minn = dfs(v, min(e[i].w, sum));
e[i].w -= minn;
e[i ^ 1].w += minn;
sum -= minn;
ans += minn;
cost += minn * e[i].c;
}
}
if(!minn) dis[u] = 0x3f3f3f3f;
vis[u] = 0;
return ans;
}
int dinic(){ //不知道叫dinic还是zkw,应该叫zkw
int ans = 0;
while(bfs()){
memcpy(now, h, sizeof(h));
ans += dfs(s, 0x3f3f3f3f);
}
return ans;
}
4. 二分图相关知识
通过构造网络流,能够解决一部分二分图题,所以这边讲讲二分图的相关理论。
4.1 二分图#
二分图:将图中的节点分为两个集合,使得所有边的两个端点不在同一集合的图就叫二分图。
性质:
- 二分图中不存在长度为奇数的环。
4.2 二分图最大匹配#
4.2.1 问题#
给定一张二分图,两个集合各有
4.2.2 做法#
题目可以转化为网络流解决:
发现没有源汇点,所以我们自己构造一组虚拟源汇点
如果
连完边大概是这样:
这样,我们就可以对源汇点跑最大流了,最大流就是最大匹配数。
值得注意的是,用网络流跑二分图最大匹配的复杂度是
4.3 二分图最小点覆盖#
4.3.1 问题#
从给定二分图中最少要选多少个点,使得每条边至少有一个端点被选中。
4.3.2 做法#
首先给出结论:最小点覆盖
证明:
假设最小点覆盖为
假设最大匹配为
综上,最小点覆盖
最小点覆盖可以解决这类问题:问至少要选多少个点才能完成所有条件(边)。
4.4 二分图最大独立集#
4.4.1 问题#
从给定二分图中选出最多的点,并且两两之间没有边相连。
4.4.2 做法#
结论:最大独立集
这个问题可以转化成二分图中选一个点,与它相连的所有点就都不能选,最多能选多少点。
这个其实就是选一个,不能选另一个的模型,容易想到最小割解决。
构造的网络和最大匹配一样,跑完最大流后,我们就求出了最小割,用
为什么呢?我们割掉一个点与源点或汇点相连的边,就表示不选这个点,为了让源汇点不连通,对于每一条路径,要么割掉源点那一条,要么割掉汇点那一条,那么我们用最少的点让源汇点不连通,剩下的点肯定是互不连通的,因此最大独立集
也可以用最小点覆盖来理解,我们用最小点覆盖了所有边,对于剩下的点,两两之间肯定不相连,反证,如果相连了,那最小点覆盖就少了这条边了,所以两两不相连,所以最大独立集
4.5 最大权值闭合子图#
4.5.1 前言#
这个东西不是二分图,但解决这个问题需要构造一张二分图。
4.5.2 问题#
给定一张有向图,点权有正有负,选一个点集,要求点集中每个点连出的边连向的点都在点集中,求权值最大的点集。
4.5.3 做法#
建图方法:我们可以将点权为正的点与源点相连,边权是点权;点权为负的点与汇点相连,边权是点权的相反数,原来的边只连正到负的边,边权是
先将答案加上所有正权边,表示假设全选了正权点,我们对构造的图求最小割,使
- 割掉从源点到正权点的边,代表不选正权点,因为原来我们已经计入正权点的贡献了,同时原图中与它相连的负权点的边,也就是负权点到汇点的边就不会被割,相当于不选负权点。
- 割掉从负权点到汇点的边,代表选负权点,同时原图中与它相连的正权点的边,也就是源点到正权点的边就不会被割,相当于选了正权点,因为开始时我们就已经把正权点计入贡献里了。
我们发现这其实就是要么选这个,要么选那个的最小割问题了,割完后源汇点是不连通的,就不会发生矛盾,答案其实就是所有正权点权值减去最小割。
结论:闭合子图最大权值
5. 上下界网络流
5.1 简介#
唯一与原来的网络流不同的是,这次的容量多了个下限
有了这个限制,我们构造的网络就不一定是合法的了,所以我们就把上下界网络流分成五部分:
- 无源汇上下界可行流
- 有源汇上下界可行流
- 有源汇上下界最大流
- 有源汇上下界最小流
- 有源汇上下界最小费用可行流
5.2 无源汇上下界可行流#
可行流需要满足流量守恒,我们假设每条边都已经流了流量为下限的流量,这时候不一定网络是合法的,有两种不合法的可能:
- 对于某一个点,流进去的总流量小于流出去的总流量,说明有一些流量被这个点私吞了。
- 对于某一个点,流进去的总流量大于流出去的总流量,说明这个点很大方,多流了一些流量。
对于这些点,有些需要再流出一些给别人,有些需要被流进一些流量,我们可以这样构图:
- 如果
,说明流进去的总流量小于流出去的总流量,我们连一条从源点到 点的边,边权为 ,表示从源点吐出要给别人的流量。 - 如果
,说明流进去的总流量大于流出去的总流量,我们连一条从 点到汇点的边,边权为 ,表示还需要让别人给多少流量。
原来的边照样连,但是边权是 上限
这样我们从虚拟源汇点跑一次最大流,如果是满流,也就是源点要流出去的流量全都到了汇点,就说明是有合法的流的。
#include <bits/stdc++.h>
using namespace std;
inline int read(){
int x = 0, f = 1;
char c = getchar();
while(c < '0' || c > '9'){
if(c == '-') f = -1;
c = getchar();
}
while(c >= '0' && c <= '9'){
x = (x << 1) + (x << 3) + (c - '0');
c = getchar();
}
return x * f;
}
int n, m, cnt = 1, s, t, sum;
int dis[100010], now[100010], h[100010], in[100010], low[100010];
struct node{
int to, nxt, w;
}e[500010];
void add(int u, int v, int w){
e[++cnt].to = v;
e[cnt].w = w;
e[cnt].nxt = h[u];
h[u] = cnt;
}
void adde(int u, int v, int w){
add(u, v, w);
add(v, u, 0);
}
bool bfs(){
queue<int> q;
for(int i = 1; i <= t; i++) dis[i] = 0x3f3f3f3f;
dis[s] = 0;
now[s] = h[s];
q.push(s);
while(!q.empty()){
int u = q.front();
q.pop();
for(int i = h[u]; i; i = e[i].nxt){
int v = e[i].to;
if(e[i].w && dis[v] == 0x3f3f3f3f){
dis[v] = dis[u] + 1;
now[v] = h[v];
q.push(v);
if(v == t) return 1;
}
}
}
return 0;
}
int dfs(int u, int val){
if(u == t) return val;
int minn, ans = 0;
for(int i = now[u]; i && sum; i = e[i].nxt){
int v = e[i].to;
now[u] = i;
if(e[i].w && dis[v] == dis[u] + 1){
minn = dfs(v, min(e[i].w, val));
if(!minn) dis[v] = 0x3f3f3f3f;
e[i].w -= minn;
e[i ^ 1].w += minn;
ans += minn;
val -= minn;
}
}
return ans;
}
int dinic(){
int ans = 0;
while(bfs()){
ans += dfs(s, 0x3f3f3f3f);
}
return ans;
}
int main(){
n = read(), m = read();
s = n + 1, t = s + 1;
for(int i = 1; i <= m; i++){
int u = read(), v = read(), lo = read(), up = read();
in[u] -= lo;
in[v] += lo; //记录流入和流出
low[i * 2] = lo; //记录下限
adde(u, v, up - lo);
}
for(int i = 1; i <= n; i++){
if(in[i] > 0){
adde(s, i, in[i]);
sum += in[i];
}
else{
adde(i, t, -in[i]);
}
}
int ans = dinic();
if(ans != sum){
cout << "NO" << endl;
return 0;
}
cout << "YES" << endl;
for(int i = 2; i <= m * 2; i += 2){
cout << e[i ^ 1].w + low[i] << endl; //每条边的流量就是原本流的下限加上反向边的流量
}
return 0;
}
5.3 有源汇上下界可行流#
与无源汇不同的就是图本身有了源汇点,而源汇点是不需要满足流量平衡的。
其实我们只需要汇点向源点连一条容量为
5.4 有源汇上下界最大流#
最大流怎么求呢,跟无源汇最大流一样跑最大流,其实
#include <bits/stdc++.h>
using namespace std;
inline int read(){
int x = 0, f = 1;
char c = getchar();
while(c < '0' || c > '9'){
if(c == '-') f = -1;
c = getchar();
}
while(c >= '0' && c <= '9'){
x = (x << 1) + (x << 3) + (c - '0');
c = getchar();
}
return x * f;
}
int n, m, s, t, cnt = 1, S, T, sum;
int dis[100010], h[100010], now[100010], in[100010], low[100010];
struct node{
int to, nxt, w;
}e[500010];
void add(int u, int v, int w){
e[++cnt].to = v;
e[cnt].w = w;
e[cnt].nxt = h[u];
h[u] = cnt;
}
void adde(int u, int v, int w){
add(u, v, w);
add(v, u, 0);
}
bool bfs(){
queue<int> q;
for(int i = 1; i <= T; i++) dis[i] = 0x3f3f3f3f;
dis[S] = 0;
now[S] = h[S];
q.push(S);
while(!q.empty()){
int u = q.front();
q.pop();
for(int i = h[u]; i; i = e[i].nxt){
int v = e[i].to;
if(e[i].w && dis[v] == 0x3f3f3f3f){
dis[v] = dis[u] + 1;
now[v] = h[v];
q.push(v);
if(v == T) return 1;
}
}
}
return 0;
}
int dfs(int u, int val){
if(u == T) return val;
int minn, ans = 0;
for(int i = now[u]; i && val; i = e[i].nxt){
int v = e[i].to;
now[u] = i;
if(e[i].w && dis[v] == dis[u] + 1){
minn = dfs(v, min(e[i].w, val));
if(!minn) dis[v] = 0x3f3f3f3f;
e[i].w -= minn;
e[i ^ 1].w += minn;
val -= minn;
ans += minn;
}
}
return ans;
}
int dinic(){
int ans = 0;
while(bfs()){
ans += dfs(S, 0x3f3f3f3f);
}
return ans;
}
int main(){
n = read(), m = read(), s = read(), t = read();
S = n + 1, T = S + 1;
for(int i = 1; i <= m; i++){
int u = read(), v = read(), lo = read(), up = read();
in[u] -= lo;
in[v] += lo;
low[2 * i] = lo;
adde(u, v, up - lo);
}
for(int i = 1; i <= n; i++){
if(in[i] > 0){
adde(S, i, in[i]);
sum += in[i];
}
else{
adde(i, T, -in[i]);
}
}
adde(t, s, 0x3f3f3f3f);
if(dinic() != sum){
cout << "please go home to sleep" << endl;
return 0;
}
int ans = e[cnt].w;
S = s, T = t;
e[cnt - 1].w = e[cnt].w = 0;
cout << ans + dinic() << endl;
return 0;
}
5.5 有源汇上下界最小流#
做法与有源汇上下界最大流一样,唯一不同的就是源汇点设为原本的
6. Trick
6.0 前言#
这些只是我遇到的一些题目建图的技巧,并不是所有。
6.1 拆点#
如果题目对点有限制,比如说点只能经过几次,我们就可以把一个点拆成一头一尾两个点,连边时,起点的尾巴连向终点的头,自己本身的头连尾巴,容量是限制的次数。
6.2 二分+网络流#
如果答案不能直接求出,而它本身的大小对于网络的连边也有一定的限制,那么可以把它作为二分的对象,每一次二分,建一张新图,跑一遍网络流,如果还能满足条件,那就还能继续往某个方向寻找答案。
6.3 分层图#
如果随着流量流向下一个点,一个东西会不断的减少,那么我们就可以以这个东西分层,比如时间,我们可以经过时间的大小,建不同层,从下层连上层,代表时间的经过。
发现时间这个限制,可以用时间分层,如果油用完了,那就只能原地建加油站,同时连边到第一层,也就是满油的层。
6.4 动态开点#
如果把所有的情况一下子全开了,会超出内存限制,并且一些点在某些情况下不需要建出来,是多余的,我们就可以边统计贡献,边开点。
是 P2053 [SCOI2007] 修车 的数据加强版,考虑将厨师每个时刻做菜的贡献不同,将厨师分成若干个点,分的点太多,会超内存,所以用动态开点,用时间换空间。
6.5 取物连重边#
如果一个东西,只有在第一次取的时候有贡献,之后虽然可以经过,但是没有贡献,考虑连两条边,一条容量为
6.6 黑白染色#
比如条件是一个点用了,某些点就不能用,这是明显的二分图问题,但图中都是正权点,可以观察性质,发现同一类的点只能限制另一类的点,就可以黑白染色。
发现
6.7 优化建边#
如果二分图里,直接两两建边,边数太多,会
这题直接建边只能得 70pts,考虑优化边数,发现两个之间能够连边的共同之处是有公因数,所以把公因数当做中转站,路径为蓝色卡片
6.8 建虚拟点#
如果某些贡献需要多个东西同时满足共同的条件,可以考虑建一个新点,将这个点连向需要满足条件的边,并且答案是可以用总贡献
源汇点分别表示文理科,朋友之间如果共同是文科或理科有额外的贡献,这个贡献可以建虚拟点,连向一对朋友,源电或汇点连向虚拟点,跑最小割,用总贡献减去需要舍弃的最小贡献就是答案。
(附)最小割树
笔者还没写......
没事,最小割树就是一颗记录着原本网络不同源汇点的割大小的树。
朴素的做法是,每次询问源点
最小割树的算法的思想就是分治
如果我们要查询随便两个点,就可以通过最小割树查询,由于树上点到点的路径是唯一的,两个点的最小割就是树中两个点路径上的最小边权,感性理解一下,要将
END
2023.1.10 写了一天,终于写完了,这篇学习笔记肯定是参考了很多神犇的博客和视频,包括 OIwiki,网络流还有一些没怎么学,比如最小割树......
总之 ,写完啦!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具