网络流
naoliaok_lovely · · 算法·理论
ps.:由于篇幅限制,本篇文章很多地方没有放图,太占空间,文章涉及到“自行推导”的字眼一般都是很显然的,读者如果不能一眼还是建议手推一下。
一、基本模型与概念
1.网络流模型
网络流模型是指一个边带权值且连通的有向图,并且该图中只存在一个入度为 0 的点(源点
2.概念
基本概念:
- 容量(Capacity):
c(u,v) 表示u\to v 这条有向边的最大流量。 - 流量(Flow):
f(u,v) 表示u\to v 这条有向边的实际流量。 - 残量:每条边的容量与流量之差。
注:本文用
性质:
- 容量限制:
f(u,v)\le c(u,v) 。 - 流量守恒:对于所有点(除了源点和汇点),流入的流量
= 流去的流量。 - 斜对称性:
f(u,v)=-f(v,u) 。
建图处理:将有向图变为无向图(没有的边容量为
ps.:鉴于我们会处理反向边,每条边与他的反向边一定要连续。
update20231207:为了建边时更加方便(也更加方便调试),建议采用如下的写法建边。本文中有一些代码是以前写的,这里就不做修改了。
int h[N], ne[M], e[M], w[M], idx = 1;
void add(int a, int b, int c)
{
e[++idx] = b, w[idx] = c, w1[idx] = d, ne[idx] = h[a], h[a] = idx;
}
void add1(int a, int b, int c)
{
add(a, b, c), add(b, a, 0);
}
二、最大流
- 路:一个元素不重复的序列,表示从源点到汇点的一条路径,相邻点必须存在有向边连接。
- 增广路:对于一条路,如果每条边的残量都大于
0 ,则称这条路为增广路。
定理:如果残留网络存在增广路,则流量可以增大。
逆定理:如果残留网络不存在增广路,则当前流量是最大流量。
P3376 【模板】网络最大流
下面将以各种算法在本题的实现进行举例。
在讲解之前,先给出各算法的效率对比。(摘自这篇博客)
| 序号 | Dinic | FF | EK | 终极 HLPP | ISAP |
|---|---|---|---|---|---|
| 1 | 0.625s | TLE | 0.171s | 0.125s | 0.265s |
| 2 | 0.562s | TLE | 0.156s | 0.093s | 0.265s |
| 3 | 0.828s | TLE | 0.625s | 0.093s | 0.390s |
| 4 | 0.578s | TLE | 0.312s | 0.093s | 0.328s |
| 5 | 2.468s | 24.000s | 0.046s | 0.078s | 0.218s |
| 6 | 5.546s | TLE | 0.078s | 0.140s | 0.203s |
| 7 | 5.218s | 10.984s | 0.109s | 0.125s | 0.328s |
| 8 | 7.812s | 49.953s | 0.218s | 0.109s | 0.265s |
| 9 | 1.281s | TLE | 0.375s | 0.078s | 0.375s |
| 10 | 0.781s | TLE | 0.156s | 0.062s | 0.187s |
| 11 | 0.312s | TLE | 0.046s | 0.093s | 0.203s |
| 12 | 0.875s | TLE | 2.703s | 0.078s | 0.328s |
| 13 | 0.703s | TLE | 0.156s | 0.156s | 0.203s |
| 14 | 0.500s | TLE | 0.328s | 0.109s | 0.218s |
| 15 | 0.296s | TLE | 0.171s | 0.109s | 0.296s |
| 16 | 0.562s | TLE | 0.234s | 0.125s | 0.296s |
| 17 | 4.687s | TLE | 0.140s | 0.093s | 0.343s |
| 18 | 2.921s | TLE | 0.031s | 0.156s | 0.296s |
| 19 | 2.359s | TLE | 0.040s | 0.078s | 0.312s |
| 20 | 4.656s | TLE | 0.078s | 0.062s | 0.390s |
| 21 | 0.500s | TLE | 0.312s | 0.093s | 0.218s |
| 22 | 1.000s | TLE | 0.203s | 0.109s | 0.234s |
| 23 | 0.343s | TLE | 0.062s | 0.156s | 0.265s |
| 24 | 1.015s | TLE | 0.281s | 0.140s | 0.328s |
| 总用时 | 46.428s | - | 7.037s | 2.553s | 6.754s |
(虽然但是,怎么 EK 和 ISAP 一样快啊喂!!!)
1.FF(Ford-Fulkerson) 算法
对增广路进行深搜,直到不存在增广路。每次找到一条增广路后,我们都沿着这条路走一遍。设这条路的最小残量为
时间复杂度:
由于效率奇高无比,这里就不给代码了,与下面 EK 相似。
2.EK(Edmonds-Karp) 算法
对增广路进行宽搜,直到不存在增广路。
时间复杂度:
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int N = 210;
int n, m, S, T;
LL d[N][N], ans;
LL flow[N];
int pre[N];//记录路径
int q[N], hh, tt;
bool EK()
{
memset(flow, 0, sizeof(flow));
flow[q[hh = tt = 1] = S] = 1e18;
while(hh <= tt)
{
int t = q[hh++];
if(t == T) return true;
for(int i = 1; i <= n; i++)
if(d[t][i] && !flow[i])
flow[q[++tt] = i] = min(flow[t], d[t][i]), pre[i] = t;
}
return false;
}
int main()
{
cin >> n >> m >> S >> T;
for(int i = 1, a, b, c; i <= m; i++)
cin >> a >> b >> c, d[a][b] += c;
while(EK())
{
ans += flow[T];
for(int i = T; i != S; i = pre[i])
d[pre[i]][i] -= flow[T], d[i][pre[i]] += flow[T];
}
cout << ans << endl;
return 0;
}
链式前向星版:
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int N = 210, M = 10010;
int n, m, S, T;
LL ans;
int h[N], ne[M], e[M], w[M], idx = 1;
void add(int a, int b, int c)
{
e[++idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx;
}
LL flow[N];
int pre[N];//记录路径
int q[N], hh, tt;
bool EK()
{
memset(flow, 0, sizeof(flow));
flow[q[hh = tt = 1] = S] = 1e18;
while(hh <= tt)
{
int t = q[hh++];
if(t == T) return true;
for(int i = h[t]; i; i = ne[i])
if(w[i] && !flow[e[i]])
flow[q[++tt] = e[i]] = min(flow[t], (LL)w[i]), pre[e[i]] = i;
}
return false;
}
int main()
{
cin >> n >> m >> S >> T;
for(int i = 1, a, b, c; i <= m; i++)
cin >> a >> b >> c, add(a, b, c), add(b, a, 0);
while(EK())
{
ans += flow[T];
for(int i = T; i != S; i = e[pre[i] ^ 1])
w[pre[i]] -= flow[T], w[pre[i] ^ 1] += flow[T];
}
cout << ans << endl;
return 0;
}
3.(I)SAP((Improved) Shortest Augmenting Paths) 算法
注意到,我们在上面搜的过程中,每次搜到一条增广路,就要从头开始。这十分冗余,考虑仅仅回退一步。实现的时候,可以记录
在实现的时候,我们不需要单独去处理
时间复杂度:
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int N = 210;
int n, m, s, t;
LL d[N][N], ans;
int dis[N], cnt[N];//这两个数组要注意多测清空
LL sap(int x, LL inflow)
{
if(x == t) return inflow;
LL outflow = 0;
for(int i = 1; i <= n; i++)
if(d[x][i] && dis[x] == dis[i] + 1)
{
LL flow = sap(i, min(inflow - outflow, d[x][i]));
d[x][i] -= flow, d[i][x] += flow, outflow += flow;
if(inflow == outflow || dis[s] >= n) return outflow;
}
if(!--cnt[dis[x]]) dis[s] = n;
cnt[++dis[x]]++;
return outflow;
}
int main()
{
cin >> n >> m >> s >> t;
for(int i = 1, a, b, c; i <= m; i++)
{
scanf("%d%d%d", &a, &b, &c);
d[a][b] += c;
}
while(dis[s] < n) ans += sap(s, 1e18);//一般来说,这个最大 dis 取总点数
cout << ans << endl;
return 0;
}
链式前向星版:(埋下伏笔)
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int N = 210, M = 10010;
int n, m, s, t;
LL ans;
int h[N], ne[M], e[M], w[M], idx = 1;
void add(int a, int b, int c)
{
e[++idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx;
}
int dis[N], cnt[N];
LL sap(int x, LL inflow)
{
if(x == t) return inflow;
LL outflow = 0;
for(int i = h[x]; i; i = ne[i])
if(w[i] && dis[x] == dis[e[i]] + 1)
{
LL flow = sap(e[i], min(inflow - outflow, (LL)w[i]));
w[i] -= flow, w[i ^ 1] += flow, outflow += flow;
if(inflow == outflow || dis[s] >= n) return outflow;
}
if(!--cnt[dis[x]]) dis[s] = n;
cnt[++dis[x]]++;
return outflow;
}
int main()
{
cin >> n >> m >> s >> t;
for(int i = 1, a, b, c; i <= m; i++)
scanf("%d%d%d", &a, &b, &c), add(a, b, c), add(b, a, 0);
while(dis[s] < n) ans += sap(s, 1e18);
cout << ans << endl;
return 0;
}
而前面的“improved”,增加了一个当前弧优化:注意到,对于菊花图,上面代码的运行效率可能稍差。我们可以增加一个标记
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int N = 210, M = 10010;
int n, m, s, t;
LL ans;
int h[N], ne[M], e[M], w[M], idx = 1;
void add(int a, int b, int c)
{
e[++idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx;
}
int dis[N], cnt[N], cur[N];
LL sap(int x, LL inflow)
{
if(x == t) return inflow;
LL outflow = 0;
for(int &i = cur[x]; i; i = ne[i])
if(w[i] && dis[x] == dis[e[i]] + 1)
{
LL flow = sap(e[i], min(inflow - outflow, (LL)w[i]));
w[i] -= flow, w[i ^ 1] += flow, outflow += flow;
if(inflow == outflow || dis[s] >= n) return outflow;
}
if(!--cnt[dis[x]]) dis[s] = n;
cnt[++dis[x]]++, cur[x] = h[x];
return outflow;
}
int main()
{
cin >> n >> m >> s >> t;
for(int i = 1, a, b, c; i <= m; i++)
scanf("%d%d%d", &a, &b, &c), add(a, b, c), add(b, a, 0);
for(int i = 1; i <= n; i++) cur[i] = h[i];
while(dis[s] < n) ans += sap(s, 1e18);
cout << ans << endl;
return 0;
}
4.Dinic 算法
与上面的 SAP 类似,将
时间复杂度:
从上面的表可以看出,大多数情况其实都不如 ISAP,所以平时还是用 ISAP 吧。
5.HLPP(Highest Label Preflow Push) 算法
全称叫“最高标号预流推进算法”,简称“预流推进”算法,相对上面的几种更复杂,但是也更快。一般情况下,ISAP 就够用了,当然也不乏会有缺德的出题人……
现在还没遇到,可以去 P4722 【模板】最大流 加强版 / 预流推进 自学。
时间复杂度:
6.建图技巧
网络流的题目,难就难在建图上。这里整理了常见的建图技巧。
- 拆点:我们知道,网络流中唯一的限制条件是容量,而他只能限制边上的最大流量。那如何限制点上的最大容量呢?这个时候可以使用拆点的方式,把同一个点拆成两个,并连上单向边
u\to v ,c(u,v) 就是限制的点的最大容量。 - 虚点:我们常常需要通过创建虚点的方式来辅助建图,最经典的是虚拟源点和虚拟汇点。
- 矩阵模型:有一大类题是在给定的矩阵上建图。这时一般有两种建图方式:给每一个点编号并暴力连边;把每行、每列视作一个点,再互相连边。
- 分层图模型:对于明显的分层图,一种提高效率的方式是直接在残留网络上继续增广。每次增广前先将
dis 数组暴力清空。复杂度稍加思考可以发现,如果层数多则每层的点少;每层的点多则层数少。这使得我们增广路的长度是可以保证的。(直接跑即可)
7.例题
基础运用:
P2472 [SCOI2007] 蜥蜴
需要先转化题意,再套用最大流。P1402 酒店之王
拆点的经典运用。NKOJ3517 宝藏
表面上一看是一道上下界,但是发现题目的特殊性质,稍加转化即可。用到了上面说的将每行每列看做一个点的技巧。
进阶运用:
P3163 [CQOI2014] 危桥
不再那么模板,需要一些巧妙的转化。首先将“往返”转化成“前往”,次数\times2 即可。至于对应源点到对应汇点的限制,则通过交换源汇点的方式处理。
具体的,对于普通的桥,用并查集直接处理;对于危桥,建出对应的图,然后跑a1,b1 为源点,a2,b2 为汇点的网络流,如果不行,不然无解;否则交换b1,b2 ,再跑一遍即可。
三、最小割
1.概念与定理
- 割:是网络中的一个划分,将所有点划分为
S 和T 两个点集,记为Cut(S,T) 。其中,源点s\in S ,汇点t\in T 。对于点i ,若s 可以到达i ,则i\in S ;若i 可以到达t ,则i\in T 。 - 割边:对于边
u\to v 满足u,v 两个端点属于不同的集合。 - 正向割边:
u\in S,t\in T 。 - 逆向割边:
u\in T,t\in S 。 - 割的容量:所有正向割边的容量总和。
定理一:如果
推论一:如果
推论二:网络中的最大流不超过任何割的容量。
定理二:如果
定理三:在任意网络中,最大流的值等于最小割的容量。
推论:在任意网络中,设
关于最小割为什么不能随意的连双向边的讨论
之前的最大流对于大部分的边连双向边都是没有问题的。(当然可能有例外)但是对于最小割情况就反过来了:大部分的边如果盲目的连成了双向边会导致答案错误。这使得很多初学者感到不解,这里记录下自己的见解,若有不当欢迎指出。
我们做最小割的题目是由于“最小割等于最大流”这个定理成立,从而转化成最大流处理的。但是请注意,这里的“最小割”是指的“正向割边的容量总和”。但是如果连反向边,很有可能导致一些反向割边错误的计算进了答案。所以,只有在保证多连的那条边一定不会被割掉或者一定是反向割边时,才能加入。
然而对于大部分的图,那些节点有可能属于S ,也有可能属于T ,这时的反向边自然错误。
2.寻找最小割上的边
【法一】:先求出最大流
【法二】:在残量网络中,通过 bfs 直接求得
(拓展)直接在残留网络上跑,可能会存在点不属于任何集合。这代表着最小割的方案不唯一,将这个点归入任意集合都是一种方案。(当然两个集合得各自连通)
3.例题
基础运用:
P1344 [USACO4.4] 追查坏牛奶 Pollutant Control
先看第一问,题目要求最小割,直接跑最大流即可。
第二问比较经典,求的是边数最少的最小割。处理方式是,先将题目的边权改为c_i\gets1001c_i+1 ,再跑最大流。最后算出来的最大流就是\lfloor\frac {maxf}{1001}\rfloor ,边数就是maxf\bmod1001 。(因为最多1000 条边)
第三问洛谷上被删掉了,NKOJ1852 [USACO4.4.2]Pollutant Control追查坏牛奶,问的是给出第二问中字典序最小的方案。这时就要用到上面的法一了,虽然粗暴,但也最实用。
染色模型:
P3756 [CQOI2017] 老C的方块
据说是巨佬 nodgd 出的,不愧为网络流神题,也算是正式踏入网络流的必做之题。这里不想以很简单的几句话就将这道好题敷衍过去,但是要想说清楚要占用很大的篇幅,故请读者自行阅读题解。P1935 [国家集训队] 圈地计划
相比于上面那道简单一点,但是做出了上面那道不代表一定可以做出这道。
看到“相邻边”,很容易想到通过“割”的方式处理。将原题的“最大收益”转化为“最小亏损”(先获得所有收益),把那些亏损都体现在图上。这样一说感觉像一道水题,真正写起来就发现问题了:
每个点显然都会先向源点、汇点连边权为a,b 的边,这没有问题。接着将这些点相连,代表相邻关系。但是仔细一想,这一部分的边权似乎应该为-c ???然后就可以开始罚坐哩~~~
事实上,由于整张图为二分图,仍然进行染色处理。然后,我们强行令黑色方格的类型反转。这样做就可以让上面的-c 变成+c 了。
链式模型:
P3227 [HNOI2013] 切糕
一道经典的链式建图题目。每个点都有若干个高度可以选,于是我们把这些高度拉成一条链,把点权放到边上,用切断边来表示这个点选择了这个高度。NKOJ8314 tower
这题相信很快可以看出“链”在哪里——每座炮塔能够攻击到的所有格子形成了一条链。问题在于如何控制“路径不相交”这个恶心条件。这里要用到一个技巧“反链”,即把链反着连。操作如下:
首先考虑所有炮塔连接源点。手搓一下发现,似乎倒着连可以解决问题?于是把所有炮塔连接汇点,发现还是差一些信息。事实上,我们把所有横着的链正着连,竖着的链反着连。这样做的好处在于,一旦出现相交,那么-3,-4 的炮塔一定可以到达-1,-2 的炮塔。于是,我们把-1,-2 的炮塔连汇点,-3,-4 的炮塔连源点即可。
倍增/树剖建图:
CF768E ALT
这道题的暴力应该挺简单的,把人看做 A 类点,边看做 B 类点,用上面说的割掉一条边来表示选择。展现到图上就变成了 A 类的每一个点与他覆盖的每一条边所对应的点连边,边权为无穷大即可。
但是这样的边数是O(nm) 的,瓶颈在于上面那些表示覆盖的边。注意到这时我们不用暴力连边,通过建虚点的方式来减少我们的边数即可。
综合应用:
P3511 [POI2010] MOS-Bridges
做了这道题之后发现最小割的建图似乎确实比普通的最大流难一点?(考法灵活了很多
首先回忆有向图欧拉回路的充要条件:
- 除去所有孤立点(没有连边的点),整张图连通(无向图意义下);
分析题目,一眼二分答案。条件一很好检验,条件二就比较棘手了。题目的意思是,每条边只能走一次,也就是说我们要给无向边定向。祭出我们常见套路,先随便给他定一个向,考虑改变这条边方向带来的影响。最简单的,假设此时的方向就是正确答案,考虑如何验证:连边
s\xrightarrow{in}i,i\xrightarrow{out}t ,判断maxf=\sum in 即可。
在草稿纸上稍加推导可以发现,对于那些双向边,为了进行反向操作,我们可以连一条i\xrightarrow{2}j 的边。注意到,如果f(i,j)=0 ,意味着这条边不反向;f(i,j)=2 则反向。但是f(i,j)=1 是不合法的,我们不希望他出现!
接下来的转化极为巧妙:直接将图上的所有容量\div2 !这一步看起来荒谬至极,但仔细思考,正确性不难证明。我们上面的i\xrightarrow{2}j 的边,显然是不会改变奇偶性的(只会流0,2 嘛,1 不合法)。所以,对于一个点,如果他的in_i,out_i 奇偶性不同,显然无解;否则如果同为奇数,那肯定会有f(s,i,t)\ge1 ,我们让c(s,i)-1,c(i,t)-1 。经过了上面的操作,连接源点汇点的边权也都变成了偶数,这时给他暴力\div2 ,就可以避免之前说的f(i,j)=1 的问题。NKOJ8316 买酒卖酒
检验是否学懂了最小割的题目:)
首先对于30\% ,直接暴力建图跑最大流即可。边数是O(n^2) 级别的,仔细思考,发现其实没有什么优化建图技巧可以使用的。
那怎么办呢?相信此时会有这样的疑惑:这道题明明是最大流,和最小割什么关系?事实上,这道题需要走出一个误区:我们都知道等式是双向的。我们以前解题都在用最大流求最小割,可曾想过其实可以用最小割解决最大流问题呢?
问题转化为求最小割,这其实就是所谓的“模拟最大流”。由于这张图很特殊,求最小割其实是可以做到O(n^2) 解决的——没错,动态规划!设状态f_{i,j} 表示当前讨论到前i 个点,有j 个点属于S 集合的最小代价。转移式也很简单,f_{i,j}=\min(f_{i-1,j}+jm+a_i,f_{i-1,j-1}+b_i) 。事实上,这个方法也可以称为模拟最小割。
4.对偶图最短路
- 平面图:如果图
G 能画在平面S 上,即除顶点外无边相交,则称G 可平面嵌入S ,G 为可平面图或平面图。画出的没有边相交的图称为G 的平面表示或平面嵌入。- 对偶图:设
G 是平面图的某一个平面嵌入,构造图G' :
- 在
G 的每个面R_i 中放置G' 的一个顶点v_i' 。- 设
e 为G 的一条边,若e 在G 的面R_i 和R_j 的公共边界上,做G' 的边e' 与e 相交,且e' 连接G' 的顶点v_i',v_j' ,即e'=(v_i', v_j') ,e' 不与其他任何G' 中的边相交。若e 为G 中的桥且在R_i 的边界上,则e' 是连接R_i 的顶点v_i' 的自环,即e'=(v_i',v_i') 。——摘自 OI-wiki,做了部分描述上的修改。
既然叫对偶图,显然是两两成对的。容易证明,对偶图的对偶图为原图。
更多的内容感兴趣可以自行了解,但是在 OI 中,记住结论就行了:
原图的最小割与对偶图的最短路相等。
证明略。
解释:此处的“最小割”大家都懂,但是“最短路”指的是什么?源点和汇点是什么呢?
设原图的源点为
这有什么作用呢?我们知道,最大流算法的复杂度是比较高的,其实很多“正常”的数据范围都跑不了。例如下面的例题,显然都可以用最大流算法写,但是无法通过。而改用最短路算法可以做到
P4001 [ICPC-Beijing 2006] 狼抓兔子
事实上,如何建对偶图,这是一个不简单的工作。目前用的比较多的叫最小左转法,但是也不简洁。所以在 OI 中,大多数对偶图的题目都是方格状的,像此题一样。
按照上述说的,建出对偶图,跑 dijkstra 即可。(为啥不用 spfa?呵呵)
习题:P2046 [NOI2010] 海拔,P7916 [CSP-S 2021] 交通规划。
四、最大权闭合子图
- 闭合子图:一个有向图中,选取一些点构成点集
V ,如果V 中任意点的出边所指向的点也在V 中,则我们称V 为闭合子图。 - 最大权闭合子图:即点权总和最大的闭合子图。(显然,从上面的定义可以发现,只有对于一个带点权的有向图才有这一说。特点:点权可以为负!)
方法:
- 对于
w_i\ge0 ,连边s\xrightarrow{w_i}i ;对于w_i\le0 ,连边i\xrightarrow{-w_i}t 。 - 如果在原图中存在边
i\to j ,则连边i\xrightarrow{+\infty}j 。 - 答案为正点权之和
- 最大流,即ans=\sum\limits_{w_i\ge0}w_i-maxf 。
给出一个不算特别严谨,但是容易理解的证明:
引理:对于图上的任意一个割
Cut(S,T) ,S 一定是一个闭合子图。考虑反证,如果不是闭合子图,则一定存在点对
(i,j) 使得存在i\to j 的边且i\in S ,j\in T 。此时,因为我们连了边权为+\infty 的边,那条边一定没有被割掉,所以s\to\dots\to i\to j\to\dots\to T 构成了一条路径,与割Cut(S,T) 矛盾。考虑怎么把这个子图的权值表示出来? 考虑割边的实际意义:对于 $s$ 出发的所有割边,代表着我们不选这个正权点;对于 $t$ 结束的所有割边,代表我们选择了这个负权点。 所以上面的公式 $\sum\limits_{w_i\ge0}w_i-cutc$ 就代表了这个方案的权值和,其中 $cutc$ 表示割的容量。 又因为 $cutc=maxf$,故命题得证。
1.例题
基础运用:
太空飞行计划问题
模板题,把E 看做正权点,I 看做负权点。将题目的包含关系对应的图建出来,则变为了上面所述模型。P3749 [六省联考 2017] 寿司餐厅
非常经典的一道题,但是没有什么技巧,所以也不能算的太难。显然那些d_{i,j} 是有偏序关系的,很快可以建出来表示正权的那些点。注意连边的时候不需要把所有有包含关系的区间都连边,只需要连[i,j]\to[i,j-1] 和[i,j]\to[i+1,j] 就好了。
观察负权值的计算公式mx^2+cx ,前面的mx^2 启示我们无论多少,只要有一个这个代号的寿司就会付钱,所以对于每一个代号建一个虚点,点权为-mx^2 即可。后面的cx 就很简单了,对于寿司d_{i,i} 赋权值-x 即可。
进阶运用:
CF103E Buying Sets
其实从最大权闭合子图的角度讲不难,但是第一步的转化很巧妙,所以可能这个题的标签为“思维题”更合适。
突破点肯定在于第一句话:这n 个子集的并集刚好是n 个元素,且任意k 个子集的并集大小\ge k 。显然这是一个二分图(左边表示集合,右边表示元素),进一步的,这句话其实可以翻译为:这个二分图存在完美匹配!(读者自证不难qwq)
于是跑完美匹配,用一个元素代表一个集合,那么选择了这个元素就必须选择这个集合的其他元素。即可对元素建图跑最大权闭合子图。
五、费用流
在最大流的基础上,每条边多了一个费用
注:本文用
- 最小费用最大流:在最大流的基础上,求最小费用。
1.SSP 算法
本质上是一个基于贪心的算法,过程如下:
- 求最小费用增广路:用 SPFA 跑最短路即可,如果残量为
0 就不能走,顺便记录一下流量。 - 将这条路径增广,更新残量。
- 重复执行,直到不存在增广路。
正确性显然。
ps:相信有人会问,用 spfa 跑最短路,如果有负环怎么办呢?可以证明,如果初始的图中没有负环,则在执行过程中一定不会出现负环。而一旦初始的图中有负环,则需要额外的处理手法,在后文会有提到。
但这玩意看起来就很慢,具体复杂度逛了一圈没找到让我满意的……
设每次找增广路的时间复杂度为
O(nm) ,该网络的最大流为f ,则最坏时间复杂度为O(nmf) 。这是一个关于值域的多项式,所以是伪多项式时间的。
可以构造m=n^2,maxf=2^{\frac n2} 的网络使得 SSP 算法的时间复杂度达到O(n^32^{\frac n2}) ,所以 SSP 算法不是多项式时间的。
——摘自 OI-Wiki
P3381 【模板】最小费用最大流
(写起来确实很像 EK)
#include<bits/stdc++.h>
using namespace std;
const int N = 5010, M = 1e5 + 10;
int n, m, s, t, ans1, ans2;
int h[N], ne[M], e[M], w[M], w1[M], idx = 1;
void add(int a, int b, int c, int d)
{
e[++idx] = b, w[idx] = c, w1[idx] = d, ne[idx] = h[a], h[a] = idx;
}
int flow[N], dis[N], pre[N];
int q[N], hh, tt;
bool in[N];
bool ssp()
{
memset(flow, 0, sizeof(flow));
memset(dis, 0x3f, sizeof(dis));
memset(in, 0, sizeof(in));
hh = tt = 0;
flow[s] = INT_MAX, dis[s] = 0, in[q[tt++] = s] = 1;
while(hh != tt)
{
int t = q[hh++]; in[t] = 0;
if(hh == N) hh = 0;
for(int i = h[t]; i; i = ne[i])
if(w[i])
{
int j = e[i];
if(dis[j] > dis[t] + w1[i])
{
flow[j] = min(flow[t], w[i]), dis[j] = dis[t] + w1[i], pre[j] = i;
if(!in[j])
{
in[q[tt++] = j] = 1;
if(tt == N) tt = 0;
}
}
}
}
return flow[t];
}
int main()
{
cin >> n >> m >> s >> t;
for(int i = 1, a, b, c, d; i <= m; i++)
scanf("%d%d%d%d", &a, &b, &c, &d), add(a, b, c, d), add(b, a, 0, -d);
while(ssp())
{
ans1 += flow[t], ans2 += flow[t] * dis[t];
for(int i = t; i != s; i = e[pre[i] ^ 1])
w[pre[i]] -= flow[t], w[pre[i] ^ 1] += flow[t];
}
cout << ans1 << ' ' << ans2 << endl;
return 0;
}
此外,其实这玩意还可以优化——多路增广!思想很简单,在求出
#include<bits/stdc++.h>
using namespace std;
const int N = 5010, M = 1e5 + 10;
int n, m, ans1, ans2;
int h[N], ne[M], e[M], w[M], w1[M], idx = 1;
void add(int a, int b, int c, int d)
{
e[++idx] = b, w[idx] = c, w1[idx] = d, ne[idx] = h[a], h[a] = idx;
}
int tot, s, t;
int flow[N], dis1[N], pre[N];
int q[N], hh, tt;
bool in[N];
bool ssp()
{
memset(flow, 0, sizeof(flow));
memset(dis1, 0x3f, sizeof(dis1));
memset(in, 0, sizeof(in));
hh = tt = 0;
flow[s] = INT_MAX, dis1[s] = 0, in[q[tt++] = s] = 1;
while(hh != tt)
{
int t = q[hh++]; in[t] = 0;
if(hh == N) hh = 0;
for(int i = h[t]; i; i = ne[i])
if(w[i])
{
int j = e[i];
if(dis1[j] > dis1[t] + w1[i])
{
flow[j] = min(flow[t], w[i]), dis1[j] = dis1[t] + w1[i], pre[j] = i;
if(!in[j])
{
in[q[tt++] = j] = 1;
if(tt == N) tt = 0;
}
}
}
}
return flow[t];
}
int dis[N], cnt[N], cur[N];
int sap(int x, int inflow)
{
if(x == t) return inflow;
int outflow = 0;
for(int &i = cur[x]; i; i = ne[i])
if(w[i] && dis[x] == dis[e[i]] + 1 && dis1[x] + w1[i] == dis1[e[i]])
{
int flow = sap(e[i], min(inflow - outflow, w[i]));
w[i] -= flow, w[i ^ 1] += flow, outflow += flow;
if(inflow == outflow || dis[s] >= tot) return outflow;
}
if(!--cnt[dis[x]]) dis[s] = tot;
cnt[++dis[x]]++, cur[x] = h[x];
return outflow;
}
int main()
{
cin >> n >> m >> s >> t;
for(int i = 1, a, b, c, d; i <= m; i++)
scanf("%d%d%d%d", &a, &b, &c, &d), add(a, b, c, d), add(b, a, 0, -d);
tot = n;
while(ssp())
{
memset(dis, 0, sizeof(dis));
memset(cnt, 0, sizeof(cnt));
memcpy(cur, h, sizeof(cur));
int flow = 0;
while(dis[s] < tot) flow += sap(s, INT_MAX);
ans1 += flow, ans2 += flow * dis1[t];
}
cout << ans1 << ' ' << ans2 << endl;
return 0;
}
2.例题
基础运用:
P2469 [SDOI2010]星际竞速
建图好题?这里的难点在于如何表示“水流必须要途经一个点”。如果水流直接流向汇点,那就不是“经过”而是“停止”了。
事实上,我们可以把每一个点拆成两个点i 和i' 。还是上面说的,i\to t 要连边,同时s\to i' 也要连边。剩余的就很简单了。
动态建图:
P2050 [NOI2012] 美食节
显然,如果一个厨师要做的菜的时间依次是a1,a2\dots,a_k ,则时间为ka_1+(k-1)a_2\dots+a_k 。考虑把一个单项式看做一个整体,那么每道菜的实际时间会受到系数k 的影响。我们对厨师进行拆点,表示不同系数的厨师,则每个“单位厨师”只能做一样菜品。
正确性显然,但是时空……由于“单位厨师”显然具有单调性,只有系数为
i 的厨师被选择了,i+1 才有可能被选择。考虑动态开点、动态连边即可。
通项:
https://www.luogu.com.cn/record/138575712
5.圆桌问题
简单题,把人当做水,建虚拟点
https://www.luogu.com.cn/record/136813729
6.最长不下降子序列问题
第一问显然。(关于我第一问求错了,后面两问对了这件事……)
第二问先将题目给定的偏序集建出来(建立虚拟源点和虚拟汇点)。于是题目转化为了最多可以划分成多少个长度为
接着以源点和汇点直接跑最大流显然就是答案。
第三问就是第二问的 plus 版。只要懂了第二问,那么第三问就很简单了:把源点和
事实上,这玩意不需要重新开始跑。由于在残留网络上添加新边之后,可以接着跑最大流,所以无需清空,在第二问的基础上继续加即可。
https://www.luogu.com.cn/record/136661434
7.试题库问题
简单建模。
题面有些模糊,大意是每个题都有对应的类型,你可以决定将他归入哪一类(也就是最后每道题只有一种类型),最后使得每一类的题目数等于给定的值。
将类型看做左部的点、题目看做右部的点建立二分图,每道题向他对应的类型连边,边权为
https://www.luogu.com.cn/record/136968221
8.机器人路径规划问题
网络流 24 题中唯一一道暂未解决的题目!
对于这种题,我想如果把它作为论文题看待应该更合适。另外,由于数据本身有误,我们不以通过此题作为研究目标,而应是在尽可能优秀的复杂度内解决该问题。
这里就不赘述此题了,网络上也有若干关于此题目的讨论,可以在当中获取一些关于此题目前的研究成果。
9.方格取数问题
首先将问题转化为:从方格中删去一些数,使得删掉的数字总和最小。
这道题的突破口在于二分图,因为将相邻元素相连形成的图显然是二分图。由于删掉元素本身不好处理,所以这张初始的图很难再去表示“代价”这个信息了。考虑用额外的边表示代价,即从每个点引一条边出去,边权是点权。画出这张图,思考这张图怎么表示答案。
由于图上相邻元素不能同时选,容易想到用最小割的方式表示。显然割的是点不是边,也就是说那些代表点权的边是可以割的,连接关系的边不应该割掉,权值赋值为
https://www.luogu.com.cn/record/136823472
10.餐巾计划问题
有点类似上面的 P2469 [SDOI2010]星际竞速。
https://www.luogu.com.cn/record/137799479
11.航空路线问题
首先考虑求城市数。显然最后的形态是一个环,我们将它变为两条链,那么每条边都一定是从左往右连的。其中
方案输出也不难,无非就是找到那两条链,将其中一条倒序输出即可。
注意特殊情况:
https://www.luogu.com.cn/record/138668286
12.软件补丁问题
没看出来和网络流的关系?据说可以费用流,有懂的大佬欢迎补充下。
首先数据范围一眼状压。
容易发现只有
代码实现中没有建图,因为可以直接转移。
https://www.luogu.com.cn/record/137267683
13.星际转移问题
分层图模板题?
显然答案不会太大,考虑暴枚答案。由于不同时间每个飞船的路线不一样,不太方便处理。我们可以把每个点都拆成
之前说过,跑分层图,我们不需要重新跑,只需要清空
- 跑二分,虽然带
\log ,但常数肯定比上面小; - 用 EK
\operatorname{or} Dinic,事实上,在分层图里,EK 的复杂度会减少到和 ISAP 同级,且常数显然很小,可以通过;当然 Dinic 更优,因为 Dinic 本身就是支持跑残留网络的,用不着我们 ISAP 的清空操作。
最后一句话:判无解!!!(调死我了)
https://www.luogu.com.cn/record/137598893
14.孤岛营救问题
也不需要网络流,bfs 即可解决。
鉴于 P 很小,考虑状压。于是直接 bfs 就可以过了。另外在实现的时候,没有必要区分墙和门,因为显然找不到
https://www.luogu.com.cn/record/138679559
15.汽车加油行驶问题
鉴于
注:当经过加油站的时候,强制加油,不能不加!
https://www.luogu.com.cn/record/140191315
16.数字梯形问题
又是缝合怪?都是最大费用最大流,建图不同,每一问拆开做。
- 对于第一问,实际上就是限制了每一个点的流量至多为
1 ,拆点即可。 - 第二问实际上就是把点的流量不超过
1 改为了边的流量不超过1 ,反而更简单了。 - 第三问更简单,去掉了所有的流量限制。
注意在复制代码的时候不要粗心大意,多测清空即可。
https://www.luogu.com.cn/record/140213256
17.运输问题
费用流模板题?
显然是一个二分图,点数和边数规模都不算大,直接根据题意模拟建图即可。注意,由于要求两个答案,不能直接在残留网络上面跑,只能重新建一遍图。
https://www.luogu.com.cn/record/140215085
18.分配问题
和前一道完全一样?(甚至更简单?)
https://www.luogu.com.cn/record/140215960
19.负载平衡问题
这里和上面邮票的那道题有点相似,利用的网络流的“自适应性”进行处理。每个点和
https://www.luogu.com.cn/record/140216832
20.深海机器人问题
一眼看上去有点难度,仔细一看,好像并没有规定机器人的具体起点和终点,让我自己分配?这不成水题了吗?
然后调坐标调了很久……
https://www.luogu.com.cn/record/140229723
21.最长k可重区间集问题
费用流建模题,具体的建模参考题解。
但是本人非常的菜,什么都不会,于是使用了一种超级暴力的方法。具体如下:
首先肯定要连一条链,容量为
考虑如何修改:我们要使得每一组这样的边要么都选,要么都不选才是合法的。于是我建了一个虚点
具体的,实际上这样是有正环的,跑带正环的最大费用最大流即可。
https://www.luogu.com.cn/record/138414772
22.最长k可重线段集问题
上面那道的加强版。
不用上面的奇怪解法了,我们来想一下标准解法。首先同样的画出数轴,每一个开区间我们用一条边直接连接首尾,表示选择了它。然后跑一个最大流为
回到此题,注意到,
考虑问题所在。当某条线段的
https://www.luogu.com.cn/record/141299229
23.火星探险问题
P2045 方格取数加强版
这道题挺经典的,同时作为火星探险问题的弱化版,建议先把此题做了。
回到这道题,无非就是不能经过 1,经过 2 的价值是
当然这个题恶心的是输出方案,直接对于每个机器人在图上跑一下就好了。
https://www.luogu.com.cn/record/141218320
24.骑士共存问题
题目给了一张图,其实已经算提示了:那张图告诉我们黑白染色过后一定会是一张二分图!
于是变为了二分图的最大独立集问题,实现方法很多。
https://www.luogu.com.cn/record/141220604
八、后记
感觉网络流真的非常玄乎,有些题如果不是标签都不会往这方面想。基本靠刷题,见过的多了自然就会了。
参考文献:
浅谈网络流的各种建模方法(好文力荐!)