网络流中相关习题总结1

基础概念在 上一篇

个人整理的题单:https://www.luogu.com.cn/training/273755

后续内容在 下一篇

记号:

  1. 在提及两端点情况下,用 (c,w)(c, w) 表示一条容量为 cc ,费用为 ww 的边,下同。

  2. uv,(c,w)u \rarr v, (c, w) 来表示从 uuvv 连一条容量为 cc ,费用为 ww 的边,下同, cc 有时会省略。

类型 1:基本

这个类型建图不需要太多的转换,只需要按照题意建即可。

重要 trick 拆点:可以将一个点 uu 拆成入点 uinu_{in} 和出点 uoutu_{out} ,所有连向 uu(v,u)(v, u) 连向 uinu_{in}(v,uin)(v, u_in) ,所有从 uu 连的 (u,w)(u, w) 改为从 uoutu_{out} 连:(uout,w)(u_{out} , w) ,再在 uinu_{in}uoutu_{out} 之间连边,以此限制路径经过这个点的次数和价值。

重要 trick 只取一次:若一个边(点用拆点)上有价值,但只能只取一次,可以这么连边:两个点之间,先连一条 (1,w)(1, w)ww 为价值,保证只有一次,再连一条 (,0)(\infty, 0) ,让后面可以通过。

P4013 数字梯形问题

https://www.luogu.com.cn/problem/P4013

题意:

一个梯形最上面一行有 mm 个数字,每一行比上面一行多 11 个数字,交错排列,从顶部开始,在每个数字可以向左下或者右下走,形成 mm 条路径,求以下限制下的路径上所有数字的最大和:

  1. 每条路径不能在点或者边相交;
  2. 每条路径不能在边上相交;
  3. 每条路径可以在点上或者边上相交。

解法:

拆点,源点向最上面的连边 (,0)(\infty, 0) ,最下面的向汇点连边 (,0)(\infty, 0),中间的点向左下和右下的点连边,情况 1 2 时为 (1,0)(1, 0) ,情况 3 时为 (,0)(\infty, 0),同时每个数字 ii 拆成的出点和入点之间,情况 1 连 (1,i)(1, i) ,否则为 (,i)(\infty, i)

跑最大费用最大流即可。

P1042 深海机器人问题

https://www.luogu.com.cn/problem/P4012

题意:

有一个网格,和一些机器人,机器人可在格点上向右或向上运动,一些格边上有一些有价值的物品,会被第一个机器人取走,给出若干个起点和终点,每个起点和终点可以让若干个机器人出或入。

要让所有机器人回到终点,求物品价值最大。

解法:

和上一题一样,不过这次由于物品在格边上,故不用拆点,不过注意到一个物品只能被取一次,可以这样操作:一条边从 uuvv (有方向限制),物品价值 ww,先建一条 (1,w)(1, w) ,再建一条边 (,0)(\infty, 0) ,可以保证只取一次。然后从 ss 向所有的起点连边 (x,0)(x, 0) ,其中 xx 为起点可以让多少个机器人出发,终点同理。

跑最大费用最大流即可。

P3356 火星探险问题

https://www.luogu.com.cn/problem/P3356

题意:

一个网格图,若干个机器人,机器人在格子中,从左上走到右下,有障碍,也有价值为 11 的物品,注意物品只能被取一次。

问在到达终点的机器人个数最大的情况下,被取的物品最大价值是多少,注意没有到终点的机器人取的物品价值不会被计算。

输出方案:开始时机器人都在左上,每次选择一个机器人和它所做的操作,即输出 机器人编号 操作(0 为下, 1 为右)

和上一题几乎一样,用上面介绍的 trick 建图,但这题毒瘤的地方在于要输出方案。

结合网络流的性质可以发现,如果一条边它的边权每减小了 11 ,说明有一个机器人走过。考虑到机器人并没有差别,即一个物品不在乎是那个机器人取走的,我们可以一个一个走,从起点开始,寻找一个边权改变了的边(反向边边权 >0\gt 0 ),然后走过它,把边权改回去,根据网络流的性质,不可能存在走着走着走不下去的情况,故这么做是正确的。

输出方案的参考代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
vector<pair<int, int>> edges_new[MAXN];
void export_paths(int lim) // lim 为机器人走到终点的个数,即最大流
{
// 先建成一个新图,方便处理
for (int i = 1; i <= n * m * 2; i++) {
if (i % 2 == 1) continue;
for (int j = head[i]; j; j = edges[j].nxt) {
int v = edges[j].v;
if (edges[j ^ 1].flw && v != i - 1 && v != s && v != t) {
edges_new[i == s ? s : i / 2].push_back({v == t ? t : (v + 1) / 2, edges[j ^ 1].flw});
}
}
}
for (int i = 1; i <= lim; i++) {
int u = 1;
while (u != n * m) {
for (auto& p : edges_new[u]) {
// p.second 为这条边最多经过机器人次数
if (p.second) {
if (p.first == u + 1) {
printf("%d %d\n", i, 1);
} else {
assert(p.first == u + m);
printf("%d %d\n", i, 0);
}
p.second--; // 改边权
u = p.first;
break;
}
}
}
}
}

P4066 吃豆豆

https://www.luogu.com.cn/problem/P4066

两个 PACMAN 吃豆豆。一开始的时候,PACMAN 在所有豆豆的左下方,PACMAN 走到豆豆处就会吃掉它。PACMAN 只能向右走或者向上走,行走的路线不可以相交。问最多能吃多少个豆豆。

点数 n2000n \le 2000

两条路线不能相交的限制并没有什么用处,只要不在有豆豆的地方相交,就可以这样:

1

那么就像上一题一样了,可以从任意的点开始和结束(也是为了方便),从 ss 向所有的点连边,tt 同理。考虑到 nn 很大,不能暴力连边,如果有 uv,vwu \rarr v, v \rarr w 显然就没有必要 uwu \rarr w 了。

建图详见代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
for (int i = 1; i <= n; i++) scanf("%d%d", &a[i].first, &a[i].second);
sort(a + 1, a + n + 1);

for (int i = 1; i <= n; i++) {
int minx = INF;
G.add_edge(ss, i * 2 - 1, 1, 0), G.add_edge(i * 2, tt, 1, 0);
G.add_edge(i * 2 - 1, i * 2, 1, -1), G.add_edge(i * 2 - 1, i * 2, 1, 0);
for (int j = i + 1; j <= n; j++) {
if (a[j].second >= a[i].second && a[j].second < minx) {
G.add_edge(i * 2, j * 2 - 1, INF, 0);
minx = a[j].second;
}
}
}

类型 2:进阶——不明显的图

有些网络流题目的图不会那么明显。

P2770 航空路线问题

给定若干个城市,从西到东排列,城市之间有一些航线,判断是否存在一条最长航线满足以下要求,若满足,输出最多访问的城市个数和方案:

  1. 从最西端的城市出发,从西向东到最东端的城市,在从东向西回到最西端的城市。

  2. 除了最东端和最西端的城市,每个城市只能访问一次。

显然可以建图,把城市之间的航线看作边,要求的航线看作路径。

一条折回来的航线不好考虑,改为两条从起点出发、不相交的自西向东的航线。

每个点只访问一次的限制可以拆点解决,最西边、最东边的点可以访问两次,其他的点只能访问一次。

故综上,建图方案为:

s1in,(2,0)s \rarr 1_{in}, (2, 0)1in1out,(2,1)1_{in} \rarr 1_{out}, (2, 1)nint,(2,0)n_{in} \rarr t, (2, 0)nin1out,(2,1)n_{in} \rarr 1_{out}, (2, 1)

2in1,iiniout,(1,1)2 \le i \le n-1, i_{in} \rarr i_{out}, (1, 1)

航线 (x,y)(x, y) 连边 xoutyin,(1,0)x_{out} \rarr y_{in}, (1, 0)

求最大费用最大流。

注意:只有两个点时,最大流为 11 ,但有可行方案( 1211 \rarr 2 \rarr 1 ),注意特判。

输出方案在之前有提及,就是跳被修改过的边就可以了。

P2766 最长不下降子序列问题

https://www.luogu.com.cn/problem/P2766

给定一个序列,

  1. 计算其最长不下降子序列的长度 ss

  2. 如果每个元素只允许使用一次,计算从给定的序列中最多可取出多少个长度为 ss 的不下降子序列。

  3. 如果允许在取出的序列中多次使用 x1x_1xnx_n ,计算序列中最多能取出多少个不同的长度为 ss 的不下降子序列。

对于一个序列 a=a1,a2,ana = a_1, a_2, \cdots a_n ,如果有一个序列 pp 满足 1p1<p2<<pkn1 \le p_1 \lt p_2 \lt \cdots \lt p_k \le n ,那么序列 b=ap1,ap2,apkb = a_{p_1}, a_{p_2}, \cdots a_{p_k} 为序列 aa 的子序列。

对于序列 aa 的子序列 bbcc ,分别是由 ppqq 构造的,称 bbcc 不同,当且仅当 pp 不等于 qq ,即 pq1ip,piqi|p| \not= |q| 或 \exists 1\le i \le |p|, p_i \not= q_i

求最长不下降子序列的长度可以由 dp 完成,设 fif_i 为最后一个数是 aia_i 的最长不降子序列长度,那么 fi=maxj=1i1fj+1,aiajf_i = \max_{j=1}^{i-1} f_j + 1, a_i \ge a_js=maxfis = \max f_i

我们把选出一条长度为 ss 的不降子序列看作选出一条路径。

把一个点 ii 拆点,从入点向出点连一条边,容量为 11 ,保证只有一条路径,即一个序列选择这个点。

然后从源点向所有 fi=1f_i = 1ii 的入点连边,从所有 fi=sf_i = sii 的出点向汇点连边,然后对于所有 ii 的出点向 fj=fi+1,j>if_j = f_i + 1, j \gt i 的入点连边。以上所有的边权都是 11

以样例为例

跑一次最大流,就是第二问的答案。

对于第三问,只要把从 s1in,1in1out,ninnout,noutts \rarr 1_{in}, 1_{in} \rarr 1_{out}, n_{in} \rarr n_{out}, n_{out} \rarr t 的边的容量改为 \infty 即可。

第三问中的「不同」的限制只是限制位置不同,而这已经被 iji \rarr j 之间的容量为 11 的条件限制住了,故不用额外考虑。

P3358 最长k可重区间集问题

https://www.luogu.com.cn/problem/P3358

有若干个开区间,给定他们的左右端点,要求从其中选出一些,使得对于任意的 x0x_0 ,至多 kk 个开区间包含 x0x_0

求最长的区间长度之和。

考虑要如何处理这两个限制:最多 kk 个区间,和如果选了这一个区间,就会影响从左到右的若干个点。

考虑这么建图:

对于区间 (l,r)(l, r) ,从 llrr 连一条容量为 11 ,费用为 (l,r)|(l, r)| 的边,在相邻的点从左向右连一条容量为 kk ,费用为 00 的边。然后从源点向最左边的点连一条容量为 kk ,费用为 00 的边,从最右边的点向汇点连一条容量为 kk ,费用为 00 的边,然后跑最大费用最大流。

以样例为例

为什么是对的呢?考虑将选择的区间分成 kk 组,每组之间两两不交,就相当于从源点向汇点找 kk 条路径,每条路径不同时经过两个区间,每个区间只用一次,恰好,我们的建图就能满足这个条件,同时显然满足条件的情况下,多加入一个区间会使答案更优,所以最大费用最大流是正确的。

P3357 最长k可重线段集问题

https://www.luogu.com.cn/problem/P3357

有若干的开线段,给定两端点的坐标,要求从其中选出一些,使得对于任意的 x0x_0 ,至多 kk 个线段和 x=x0x = x_0 相交。

求最长的线段长度(一条线段的距离定义为两端点的欧几里得距离下取整)。

显然和上一题很像,对于每一条线段,显然可以将他们看作在 xx 轴上的投影,但注意有特殊情况,即垂直于 xx 轴的线段,它们按照上一题的建图方式建出来是不能限制只取 kk 个的。

考虑拆点:

假如一条线段的两端点 xx 坐标不同,从左端点的 xx 坐标对应的出点连到右端点的 xx 坐标对应的入点。如果相同,就从 xx 坐标对应的入点连到出点,然后把剩下的串起来就可以了:iiniout(i+1)in(i+1)outi_{in} \rarr i_{out} \rarr (i+1)_{in} \rarr (i+1)_{out} \rarr \cdots

P3980 志愿者招募

有一个工作共 nn 天,第 ii 天的工作需要 aia_i 人,同时可以有 mm 种志愿者,第 ii 种志愿者可以从 sis_i 天工作到 tit_i 天(每天都来,不能拆开来),每人共需要 cic_i 的工钱。

问最小花费。

这题和上两题类似,不过上两题是至多,这题至少。

考虑用流量限制人数,对于每个 ii ,从 sis_iti+1t_i + 1 连一条 (,ci)(\infty, c_i) 的边,同时取一个较大的数 MAXMAX ,从 iii+1i + 1 连一条 (MAXai,0)(MAX - a_i, 0) 的边,然后连 s1,(MAX,0)s \rarr 1, (MAX, 0)n+1t,(MAX,0)n + 1 \rarr t, (MAX, 0) ,然后跑最小费用最大流。

为了让网络跑到最大流 MAXMAX ,对于 ii+1i \rarr i + 1 ,其容量为 MAXaiMAX - a_i ,就要在经过这段的志愿这的边至少流过 aia_i ,这就满足了每天要有 aia_i 个人的限制了。

解释

类型 3:分层图

把分层图的思想引入网络流,可以记录更多的信息。

P4099 汽车加油行驶问题

https://www.luogu.com.cn/problem/P4099

有一个网格,一辆汽车在格点和边上行驶。汽车从起点 (1,1)(1, 1) 出发驶向右下角终点 (n,n)(n, n)

在若干个格点处,设置了油库,可供汽车在行驶途中加油。汽车在行驶过程中应遵守如下规则:

  1. 出发时汽车已装满油,装满油的汽车能够行驶 kk 条网格边。

  2. 汽车若向左或向上行驶,需付费用 BB

  3. 汽车在行驶过程中遇油库则应加满油并付加油费用 AA

  4. 在需要时可在网格点处增设油库,并付增设油库费用 CC (不含加油费用AA )。

问从起点行驶到终点的最小费用。

n100,k10n \le 100, k \le 10

不能用边的容量等条件限制汽车的油亮,考虑使用分层图。

连边如下,点 (x,y,p)(x, y, p) 汽车在 (x,y)(x, y) ,还有 pp 的油。

起点和终点:s(1,1,k),(1,0); (n,n,p)t,(1,0)s \rarr (1, 1, k), (1, 0);\ (n, n, p) \rarr t, (1, 0)

中间的点:1xn,1yn1 \le x \le n, 1 \le y \le n(x,y)(x', y')(x,y)(x, y) 相邻,需要费用 wwwwBB00 )。

如果 (x,y)(x, y) 有油库,则 (x,y,p)(x,y,k1),(,w+A)(x, y, p) \rarr (x', y', k - 1), (\infin, w + A)

否则,若不设油库, (x,y,p)(x,y,p1),(,w)(x, y, p) \rarr (x', y', p - 1), (\infin, w) ,若设油库 (x,y,p)(x,y,p1),(,w+C)(x, y, p) \rarr (x', y', p - 1), (\infty, w + C) (绕一圈回到原来的地方显然是不优的,所以不必担心设油库的费用 CC 被算两次)。

P2754 家园 / 星际转移问题

https://www.luogu.com.cn/problem/P2754

把若干个人从地球转移到月球,有 nn 个太空站, mm 艘太空船在地球,月球,太空站之间运送旅客,第 ii 个太空站编号 ii ,为方便,地球编号 00 ,月球编号 1-1

设第 ii 艘太空船,若它的路线为 (x1,x2,xn)(x_1, x_2, \cdots x_n) ,那么它会依次停靠 x1,x2,,xn,x1,x2,,xn,x_1, x_2, \cdots, x_n, x_1, x_2, \cdots, x_n, \cdots 号太空站(或地球或月球),它可以容纳 hih_i 个人。

问至少多久可以将所有人从地球运到月球。

使用分层图的思路,设 (i,k)(i, k) 为编号为 ii 的地方在第 kk 时刻的节点。

从第 00 个时刻开始,每个时刻 kk ,对于每个地方,新建一个节点,(i,k1)(i,k),(0)(i, k-1) \rarr (i, k), (0) (月球除外, (1,k)(1,k1)(-1, k) \rarr (-1, k-1) ),然后对于每个飞船 ii ,设其上个时刻所在的位置为 pi,k1p_{i, k-1} ,这个位置在 pi,kp_{i, k} ,那么 (pi,k1,k1)(pi,k,k),(hi)(p_{i, k-1}, k-1) \rarr (p_{i, k}, k), (h_i)

不断重复这个操作,知道这个图的最大流大于等于人数即可(添加新的边,最大流肯定是不降的,所以不用清空,直接跑就行了)。

P2053 修车

https://www.luogu.com.cn/problem/P2053

nn 辆车,分给 mm 个人修,第 ii 个人修第 jj 辆车需要 ti,jt_{i,j} 的时间,第 ii 辆车的等待时间是修它的时间加上选择修它那个人修之前的车的等待时间之和,求最小等待时间。

形式化地说:

ii 个人修 kik_i 辆车,分别为 ai,1,ai,2,ai,kia_{i, 1}, a_{i, 2}, \cdots a_{i, k_i} ,其中 ki=n\sum k_i = na1,1,a1,2a1,k1,ai,jam,kma_{1, 1}, a_{1, 2} \cdots a_{1, k_1}, \cdots a_{i, j} \cdots a_{m, k_m} 互不相同,且在 11nn 之间。

mini=1mj=1kil=1jti,ai,ln\min \frac {\sum_{i=1}^m \sum_{j=1}^{k_i} \sum_{l=1}^j t_{i, a_{i, l}}} n

肯定先求总时间最少。

考虑使用贡献,和从后往前安排顺序,如果第 ii 辆车,找 jj 修,从后往前第 kk 个,那么它对答案的贡献就是 tj,ikt_{j, i} k

解释

二分图,左边是 nn 个人,右边是 mnm * n 个点,右边的点 (j,k)(j, k) 表示找 jj ,从后往前第 kk 个修,然后左边的 ii 向右边的 jj 连边 (1,tj,ik)(1, t_{j, i}k) ,然后从源点向左边连 (1,0)(1, 0) ,从右边向汇点连 (1,0)(1, 0)

跑最小费用最大流就可以了。

P2050 美食节

这一题和上一题类似,不过数据范围较大。

我们发现不是所有的 (j,k)(j, k) 都是有用的,所以我们最开始只建 (j,1)(j, 1) ,然后假如 (j,k)(j, k) 被用了,再建 (j,k+1)(j, k + 1)

可以参考代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
inline pair<int, int> dinic()
{
int fsum = 0, csum = 0;
while (spfa()) {
while (true) {
pair<int, int> p = dfs(S, INF);
int x = p.first, y = p.second;
fsum += x, csum += y;
if (!x) break;
}
for (register int j = 1; j <= m; j++) {
if (vis1[n + id(j, top[j])] && top[j] < sum) {
// (j, k) 被用了
top[j]++;
add_edge(n + id(j, top[j]), T, 1, 0);
for (register int i = 1; i <= n; i++)
add_edge(i, n + id(j, top[j]), 1, a[i][j] * top[j]);
// 连接 i
}
}
}
return make_pair(fsum, csum);
}