【解题报告】网络流 23 题
✅
部分题目由于比较简单,所以只记录建边方法。
较难或较巧妙的会记录思路。
以下题解中的 \(in_u\) 表示拆点后 \(u\) 的入点,\(out_u\) 表示拆点后 \(u\) 的出点。
编号 | 题目名称 | 题目模型 | 转化模型 | 题解补全情况 |
---|---|---|---|---|
1 | 飞行员配对方案问题 | 二分图最大匹配 | 二分图 | ✅ |
2 | 孤岛营救问题 | 分层图最短路径 | 最短路径 | ✅ |
3 | 汽车加油行驶问题 | 分层图最短路径 | 最短路径 | ✅ |
4 | 软件补丁问题 | 最小转移代价 | 最短路径 | ✅ |
5 | 星际转移问题 | 分层图转移 | 最大流 | ✅ |
6 | 太空飞行计划问题 | 最大权闭合图 | 最小割 | ✅ |
7 | 最小路径覆盖问题 | 有向无环图最小路径覆盖 | 最大流 | ✅ |
8 | 魔术球问题 | 有向无环图最小路径覆盖 | 最大流 | ✅ |
9 | 圆桌问题 | 二分图多重匹配 | 最大流 | ✅ |
10 | 试题库问题 | 二分图多重匹配 | 最大流 | ✅ |
11 | 分配问题 | 二分图最佳匹配 | 费用流 | ✅ |
12 | 方格取数问题 | 二分图点权最大独立集 | 最小割 | ✅ |
13 | 骑士共存问题 | 二分图点权最大独立集 | 最小割 | ✅ |
14 | 负载平衡问题 | 费用流水题模板题 | 费用流 | ✅ |
15 | 运输问题 | 费用流水题模板题 | 费用流 | ✅ |
16 | 最长递增子序列问题 | 限制性带权路径 | 最大流 | ✅ |
17 | 航空路线问题 | 限制性带权路径 | 费用流 | ✅ |
18 | 数字梯形问题 | 限制性带权路径 | 费用流 | ✅ |
19 | 最长k可重区间集问题 | 限制性带权路径 | 费用流 | ✅ |
20 | 最长k可重线段集问题 | 限制性带权路径 | 费用流 | ✅ |
21 | 深海机器人问题 | 限制性带权路径 | 费用流 | ✅ |
22 | 火星探险问题 | 限制性带权路径 | 费用流 | ✅ |
23 | 餐巾计划问题 | 线性规划网络流优化 | 费用流 | ✅ |
1. 飞行员配对方案问题
二分图最大匹配模板,没什么好说的。
要是网络流 24 题每题都这么简单就好了。
2. 孤岛营救问题
容易想到 bfs。
设 \(d_{i,j,S}\) 表示到达 \((i,j)\),拥有的钥匙集合为 \(S\) 的最少步数。
于是做完了。
3. 汽车加油行驶问题
想到拆点了,但没想到一次性拆 \(k+1\) 个点。
由于 \(k\) 非常小,显然可以这么做,\((i,j,l)\) 这个点表示到达 \((i,j)\),还剩 \(l\) 这么多汽油的状态。
每个点只要还有汽油,就向上下左右分别连容量为 \(1\) 的边,费用根据方向而定。
如果这个点是加油站,那么强制加油,即所有 \((i,j,l),~ l \lt k\) 连向 \((i,j,k)\),容量为 \(1\) 费用为 \(A\)。
如果不是加油站,且已经没有汽油了 \((i,j,0)\),那么必须修建一个加油站加油,即连向 \((i,j,k)\),容量为 \(1\),费用为 \(A+C\)。
最后到达 \((n,n)\) 即为终点,所以 \(l \in [0,k]\) 均可以流向汇点。
另外由于容量全都是 \(1\),可以直接最短路……
4. 软件补丁问题
观察到 bug 的数量最多只有 \(20\),容易联想到状压。
然后按照题目条件暴力建边,直接 dijkstra 跑最短路。
这样算下来最坏情况边数是 \(2^n \times m\),卡得比较紧。
考虑时间换空间,在 dijkstra 的时候直接遍历所有补丁,看是否能使用直接拓展状态。
5. 星际转移问题
注意力惊人,注意到答案上界一定非常小。
所以考虑对每个点建分层图,即 \((i,t)\) 表示第 \(t\) 天的 \(i\) 号点。
那么每天从上一天的该点连向这一天的该点,容量为 \(\infty\),表示可以人暂留在这个空间站。
然后把这一天运行的航班从上一层连下来,容量为对应人数。
最后把这一天的月球连接向汇点,容量为 \(\infty\)。
枚举天数并边建图边跑 dinic,在残量网络上继续跑,只要最大流 \(\geq k\) 就说明可以有 \(\geq k\) 个人在这天到达月球。
可以在残量网络上跑。
6. 太空飞行计划问题
最大权闭合图模板,但输入比较毒瘤。
源点连实验,容量为收益;实验连仪器,容量为 \(\infty\);仪器连汇点,容量为价格。
然后跑最大权闭合图即可,即用总收益减去最小割。
至于输出方案,只要一个实验在 Dinic 的过程中它的 \(dis \gt 0\),它就相当于被选中(有进行过增广),直接遍历它的所有仪器即可。
为什么不能通过源点和实验点的容量为 \(0\) 判断它选中?因为虽然最小割等于最大流,但是不一定每条边都流满。
7. 最小路径覆盖问题
首先显然的是建立二分图,对于 \(u \to v\) 这条边,将左部的 \(u\) 连向右部的 \(v\),容量为 \(1\),然后源点向左部,右部向汇点。
然后由于在二分图中,最小路径覆盖 = 点数 - 最大匹配 = 点数 - 网络最大流,所以第二问可以求出。
接着是第一问的输出方案,比较笨的方法是枚举每个左部点,如果和它有连边的右部点容量为 \(0\),说明这条边被选中(流过)了,可以用并查集维护同一个路径内的点。
8. 魔术球问题
第五题和第七题的综合。
先打表找第一问的规律,发现当 \(n=55\) 的时候答案也非常小。
所以考虑暴力建边,如果 \(j\) 可以放在 \(i\) 的头上就连边 \(i \to j\)。
然后要求把尽可能多的球串在柱子上,相当于是最小路径覆盖。
考虑和第 5 题 星际转移问题 类似的思路,由于答案上界很小,直接枚举答案并动态加边。
显然根据上一题有 最小路径覆盖 = 点数 - 最大匹配。因此只要在加入某个球后 点数 - 最大匹配(最大流) 比最小路径覆盖,即 \(n\) 还大,那么就说明不能再加了。
输出方案与上一题也类似,并查集维护即可。
9. 圆桌问题
容易想到源点连单位,餐桌连汇点,容量为人数。
然而每张餐桌不能有两个来自同一单位的人,所以每个单位向每个餐桌连边为 \(1\)。
检验最大流是否等于总人数即可,输出方案直接看流量情况。
10. 试题库问题
和第 9 题很像。
源点向每道题连容量为 \(1\) 的边,每道题向对应的种类连 \(1\) 的边。
每个种类向汇点连所需数量 \(c_i\)。
跑最大流就做完了,输出方案直接看流过哪些边即可。
11. 分配问题
容易想到每个人建一个点,每个工作建一个点。
源点向每个人连费用为 \(0\) 的边,每个工作向汇点连费用为 \(0\) 的边。每个人向每个工作连费用为效益的边。
由于每个人只能做一份工作,所以整张图每条边的容量均为 \(1\)。
分别跑最大最小费用,最大流即可。
12. 方格取数问题
相邻的两个格子不能同时被选中,那么我们把行列编号之和为奇数的放在二分图的左部,其余的放在二分图的右部。
由于需要求最大和,所以相当于是删掉最小的边,联想到最小割。
源点向左部点连边,容量为点权;右部点向汇点连边,容量也为点权。
将相邻的格子由左部向右部连边,边权为 \(\infty\)(显然不能把它割掉)。
最后答案即为总价值减去最小割价值。
13. 骑士共存问题
最开始想的是建完攻击关系图之后黑白染色,但好像不是很好搞。
所以考虑和 12 题一样给网格黑白染色,发现有攻击关系的两个方格异色。
于是源点向白色点连边,黑色点向汇点连边,黑白攻击关系之间连边。
二分图的最大独立集 = 点数 - 最小点覆盖 = 点数 - 最大匹配数 = 点数 - 网络最大流
跑最大流即可。答案即为 \(n^2 - m - flow\)。
14. 负载平衡问题
把每个点拆成入点和出点。
源点向每个入点连边,容量为它所需的货物数量,费用为 \(0\)。
每个出点向汇点连边,容量为 \(0\),费用为 \(0\)。
中间每个入点向每个出点连边,容量为 \(\infty\),费用为运输所需代价(距离)。
15. 运输问题
感觉和第 11 题很像啊,只是容量不同了而已。
源点向每个仓库连对应货物数量 \(a_i\),每个商店向汇点连对应货物数量 \(b_i\),费用均为 \(0\)。
中间的所有边都是 \(\infty\),因为可以随便流,费用为 \(c_{i,j}\)。
16. 最长不下降子序列问题
首先,容易求出 dp 数组。
由于第三问中 \(1, n\) 点可以选多次,所以考虑经典套路拆点。
\(in_i \to in_j\) 的容量设置为允许选择的次数。
然后我们对 \(a_i \lt a_j, f_i + 1 = f_j\) 的点对连边 \(out_i \to in_j\),容量为 \(1\)。
接着跑最大流即可。注意一定要判定 \(a_i \lt a_j\),原因显然。
17. 航空路线问题
做这么多题都有感觉了,看到路线先拆成入点 \(in_i\) 和出点 \(out_i\)。
然后入点出点之间连边,容量为 \(1\) 费用为 \(1\)(一个点只能经过一次,会产生贡献)。
如果是 \(1\) 或 \(n\),则容量为 \(2\),因为可以重复经过。
对于路径 \(u \to v\),建边 \(out_u \to in_v\),容量为 \(1\),费用为 \(0\)。
然后跑最大费用最大流即为答案。
要求输出方案?直接在残量网络上 dfs 求两条容量均为 \(0\) 的路径即可。
18. 数字梯形问题
每个点都有价值,然而网络流中不太好直接给点赋值。同时每个点有进入也有出去,所以考虑把每个点拆成入点和出点,建图后跑最大费用最大流。
对于建图,显然是源点向第一行的入点连边,费用为 \(0\),每个入点向对应的出点连边,费用为点权,出点向左下、右下的入点连边,费用为 \(0\),最后一行的出点向汇点连边,费用为 \(0\)。
由于需要满足三种不同限制,考虑如何定义容量可以满足条件。
- 路径互不相交
- 所有边容量均为 \(1\)。
- 实际意义:每个点只能被经过一次,每条边只能被经过一次。
- 仅在数字节点相交
- 把入点到出点的容量设为 \(\infty\),左下右下的容量仍然是 \(1\)。
- 实际意义:每个点可以被经过多次,每条边只能被经过一次。
- 注意最后一行的出点向汇点容量为 \(\infty\)。原因是需要保证最大流。
- 允许在数字节点和边相交
- 在上一点的基础上把左下右下连边的容量也设为 \(\infty\) 即可。
- 实际意义:每条边可以被经过多次。
19. 最长 k 可重区间集问题
首先,套用点权转边权的思路,把每个点拆成入点和出点,边权容量为 \(k\)。
然后区间 \([l,r]\) 转化为从 \(in_l\) 流入容量为 \(1\) 费用为 \(len\),\(out_r\) 流出容量为 \(1\) 费用为 \(0\)(只算一次)。
但这样建图的问题在于不能保证每个左端点流入的流量一定从对应的右端点流出。
于是考虑换一种建图方法,观察上述问题的本质在于重合,即所有区间的贡献在一起跑最大流。
不能重合,那么就不要重合!考虑把每条线段分流。
这次不用拆点了,首先源点向 \(1\) 号点连容量 \(k\) 费用 \(0\),就可以很好地限制每个点的覆盖次数,汇点同理。
然后每个点 \(i\) 向 \(i+1\) 连容量 \(\infty\) 费用 \(0\)。
那怎么给线段分流呢?考虑到所有的流量都在上面建好的“主干”上,那么每次选择一条线段相当于从“主干”抽出来 \(1\) 的流量。
那么对于线段 \([l,r]\),从 \(l\) 到 \(r\) 连一条容量为 \(1\),费用为 \(len\) 的有向边即可。
最后跑最大费用最大流。另外由于 \(l,r \leq 10^5\) 需要离散化。
20. 最长 k 可重线段集问题
其实就是上一题改到了二维平面上。
如果仍然套用上一题的代码,改一下权值会挂,因为会存在 x 坐标相同的情况。
这时候就需要套用拆点技巧了。拆成 \(in_u, out_u\)。
若 \(l \not= r\),则 \(out_l \to in_r\)。
否则 \(in_l \to out_l\)。
注意处理开闭区间问题了。
21. 深海机器人问题
挺简单的。每条路径可以经过多次,但价值只算一次,容易想到建一条边容量为 \(1\),费用为 \(w\);再建一条边容量为 \(\infty\),费用为 \(0\)。
对于起点直接从源点连边容量为 \(k\),费用为 \(0\);对于终点直接连向汇点,容量为 \(r\),费用为 \(0\)。
要求让尽可能多的机器人到达终点,所以跑最大费用最大流即可。
22. 火星探险问题
和上一题几乎完全一样,但是注意到改为点权了。
于是经典套路,拆点,点权转边权。
输出路径?直接 dfs 即可,如果有一条边边权没有到 \(\infty\) 说明有机器人过去了。
23. 餐巾计划问题
由于每一天可以获得餐巾,也需要消耗一定量的餐巾,所以考虑把每个点拆成入点和出点。
- 购买新餐巾从源点 \(S\) 连向 \(i\) 的入点,容量为 \(\infty\),费用为 \(p\)。
- 快洗应当从 \(i\) 的出点连向 \(i+m\) 的入点,容量为 \(\infty\),费用为 \(s\)。
- 慢洗应当从 \(i\) 的出点连向 \(i+n\) 的入点,容量为 \(\infty\),费用为 \(f\)。
- 延期送洗从 \(i\) 的出点连向 \(i+1\) 的入点,容量为 \(\infty\),费用为 \(0\)。
但显然所有边都连向入点,图是不连通的。
因此我们修改连边方式:
- 源点连向 \(i\) 的出点,容量为 \(\infty\),费用依然为 \(p\)。
- 每个 \(i\) 的入点连向汇点 \(T\),容量为当天所需餐巾数量 \(R_i\),费用为 \(0\)。
最大流模板
熟练背诵。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e4 + 15, M = 4e5 + 15;
const long long INF = 1e18;
int n, m, S, T;
int h[N], e[M], w[M], ne[M], idx = 0;
void add(int a, int b, int c) { e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++; }
void addedge(int a, int b, int c) { add(a, b, c), add(b, a, 0); }
long long ans = 0;
int d[N], cur[N];
bool bfs() {
queue<int> q; while (q.size()) q.pop();
for (int i = 1; i <= n; i++) d[i] = 0;
cur[S] = h[S], d[S] = 1, q.push(S);
while (q.size()) {
int u = q.front(); q.pop();
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
if (!w[i] || d[v]) continue;
d[v] = d[u] + 1, cur[v] = h[v];
q.push(v);
if (v == T) return 1;
}
}
return 0;
}
long long dfs(int u, long long rest) {
if (u == T) return rest;
if (rest == 0) return 0;
long long flow = 0;
for (int i = cur[u]; ~i; cur[u] = i = ne[i]) {
int v = e[i];
if (d[v] != d[u] + 1 || w[i] == 0) continue;
long long now = dfs(v, min(w[i] * 1ll, rest - flow));
if (now == 0) d[v] = -1;
else w[i] -= now, w[i ^ 1] += now, flow += now;
if (flow == rest) break;
}
return flow;
}
int main() {
scanf("%d%d%d%d", &n, &m, &S, &T);
for (int i = 1; i <= n; i++) h[i] = -1;
for (int i = 1, a, b, c; i <= m; i++) {
scanf("%d%d%d", &a, &b, &c);
addedge(a, b, c);
}
while (bfs()) ans += dfs(S, INF);
printf("%lld\n", ans);
return 0;
}
最小费用最大流模板
熟练背诵。
#include <bits/stdc++.h>
using namespace std;
const int N = 5015, M = 1e5 + 15;
const int INF = 0x3f3f3f3f;
int n, m, S, T;
int h[N], e[M], c[M], w[M], ne[M], idx = 0;
void add(int a, int b, int x, int y) { //(u, v) 容量为 x,费用为 y
e[idx] = b, c[idx] = x, w[idx] = y, ne[idx] = h[a], h[a] = idx++;
}
void addedge(int a, int b, int x, int y) {
add(a, b, x, y);
add(b, a, 0, -y);
}
bool st[N];
int Min[N], d[N], pre[N];
bool spfa() {
queue<int> q; while (q.size()) q.pop();
for (int i = 1; i <= n; i++) d[i] = INF, pre[i] = Min[i] = 0;
Min[S] = INF, d[S] = 0, q.push(S);
while (q.size()) {
int u = q.front(); q.pop();
st[u] = 0;
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
if (!c[i]) continue;
if (d[v] > d[u] + w[i]) {
d[v] = d[u] + w[i];
pre[v] = i;
Min[v] = min(Min[u], c[i]);
if (!st[v]) q.push(v), st[v] = 1;
}
}
}
return Min[T] > 0;
}
long long ans1 = 0, ans2 = 0;
void get() {
int t = Min[T];
ans1 += t, ans2 += t * 1ll * d[T];
int now = T;
while (now != S) {
int id = pre[now];
c[id] -= t, c[id ^ 1] += t;
now = e[id ^ 1];
}
}
int main() {
scanf("%d%d%d%d", &n, &m, &S, &T);
for (int i = 1; i <= n; i++) h[i] = -1;
for (int i = 1, a, b, x, y; i <= m; i++) {
scanf("%d%d%d%d", &a, &b, &x, &y);
addedge(a, b, x, y);
}
while (spfa()) get();
printf("%lld %lld\n", ans1, ans2);
return 0;
}
无源汇上下界网络流
对于一条边 \((u,v,a,b)\),表示 \(u \to v\) 的边流量必须在 \([a,b]\) 内。
建立虚拟源汇 S、T,将这条边改为 \((u,T,a), (S,v,a), (u, v, b-a)\),从 \(S \to T\) 跑一次 dinic,如果最大流是 \(\sum a\) 则可行。
有源汇上下界网络流
设要求的源汇分别为 s、t,还是建立虚拟源汇 S、T。
首先要把网络搞成循环流,使得它无源汇,只要 \(t \to s\) 容量 \(+\infty\) 即可。
然后连边和之前一样,先跑一遍 \(S \to T\) dinic,再跑一遍 \(s \to t\) dinic 就行了。
#include <bits/stdc++.h>
using namespace std;
const int N = 205, M = 1e5 + 5;
const long long INF = 0x3f3f3f3f;
int n, m, s, t, S, T;
int h[N], e[M], w[M], ne[M], idx = 0;
inline void add(int a, int b, int c) { e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++; }
inline void addedge(int a, int b, int c) { add(a, b, c), add(b, a, 0); }
int d[N], cur[N];
queue<int> q;
bool bfs(int S, int T) {
while (q.size()) q.pop();
for (int i = 1; i <= n + 2; i++) d[i] = 0;
q.push(S), cur[S] = h[S], d[S] = 1;
while (q.size()) {
int u = q.front(); q.pop();
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i]; if (!w[i] || d[v]) continue;
d[v] = d[u] + 1, cur[v] = h[v], q.push(v);
if (v == T) return 1;
}
}
return 0;
}
long long dfs(int u, long long rest, int T) {
if (!rest) return 0;
if (u == T) return rest;
long long flow = 0;
for (int i = cur[u]; ~i; cur[u] = i = ne[i]) {
int v = e[i]; if (!w[i] || d[v] != d[u] + 1) continue;
long long now = dfs( v, min(rest - flow, w[i] * 1ll), T );
if (!now) d[v] = -1;
else w[i] -= now, w[i ^ 1] += now, flow += now;
if (flow == rest) return rest;
}
return flow;
}
long long lim;
long long dinic(int S, int T) {
long long res = 0;
while (bfs(S, T)) res += dfs(S, INF, T);
return res;
}
int main() {
scanf("%d%d%d%d", &n, &m, &s, &t), S = n + 1, T = n + 2;
for (int i = 1; i <= n + 2; i++) h[i] = -1;
for (int i = 1, u, v, a, b; i <= m; i++) scanf("%d%d%d%d", &u, &v, &a, &b), addedge(u, T, a), addedge(S, v, a), addedge(u, v, b - a), lim += a;
addedge(t, s, INF);
if (dinic(S, T) ^ lim) return puts("please go home to sleep"), 0;
printf("%lld\n", dinic(s, t) );
return 0;
}