模拟费用流
模拟费用流
加速费用流,用数据结构来模拟费用流的过程
例:BZOJ 1150 [CTSC2007] 数据备份 Backup
题意:
一条街上有 \(n\) 栋办公楼,位置 \(x_1<x_2<⋯<x_n\)。你需要选出 \(K\) 对办公楼,每对之间铺设一条电缆,每栋楼最多属于一对。一条电缆的费用就是两栋楼之间的距离。求最小总费用。
第一步:写出费用流模型:
模拟费用流只是为了加速费用流,但费用流的模型仍然是一样的。
将位置相邻的办公楼之间的距离提取出来,记 \(d_i=x_i+1−x_i,i=1,…,n−1\)。
首先,电缆肯定只会建在相邻两个楼房之间,所以:
将楼房黑白染色:
-
源点向黑点连容量 \(1\) ,费用 \(0\) 的边。
-
白点向汇点连容量 \(1\) ,费用 \(0\) 的边。
-
黑点向原位置上相邻的白点连容量 \(1\) ,费用 \(d\) 的边。
第二步:模拟费用流增广
由于这个模型很简单,大致如下图:

你会发现,只有中间的边上有费用,且每条路流量都是 \(1\) 。
那么我们考虑自己来模拟费用流的过程:
-
初始建边 \(→\) 将中间所有边的权值都依次加入优先队列中,记为 \(w_i\) 。
-
单路增广 \(→\) 取中间边上的最小值,输送 \(1\) 流量, 然后把左右两条路删去(用不了了)。
-
改反向边流量 \(→\) 加入一条费用为 \(w_{pre_i}+w_{nxt_i}-w_i\) 的边用来反悔 (\(pre_i\) 表示 \(i\) 的前一条未被删除的边,\(nxt\) 反之)(这样一条边是实际费用流中多条边的超级合体,见下图)。

而这样的合体会导致合体边两侧的边不可用,上图中权值为 \(7\) 和 \(8\) 的边就无法使用了。
这就是反悔边的费用是 \(w_{pre_i}+w_{nxt_i}-w_i\) 的原因。
这样重复操作 \(K\) 次就是答案。
所以这道题就可以用链表加优先队列解决。
时间 \(O(K log\, n)\) 。
线段树模拟费用流:P4694
你需要生产 \(k\) 张光盘,每张光盘需先经 \(A\) 工厂挤压,再经 \(B\) 工厂涂反光层。每天 \(A、B\) 工厂各能加工一张,花费分别为 \(a_i、b_i\)。求生产\( k\) 张光盘的最小总花费。
一个自然的费用流模型是:将每一天抽象为一个点 \(d_i\),建立源点 \(S\)、汇点 \(T\) 及一个容量为 \(k\) 的超级源点 \(U\):
-
\(U→S\):容量 \(k\),费用 \(0\)。
-
\(S→d_i\):容量 \(1\),费用 \(a_i\)(选择 A 工厂)。
-
\(d_i→T\):容量 \(1\),费用 \(b_i\)(选择 B 工厂)。
-
\(d_i→d_{i+1}\):容量 \(+∞\),费用 \(0\)(表示半成品可以留到以后加工)。
直接跑费用流会超时,需要模拟其增广过程。
关键:观察增广路的结构
考虑如下操作,起初我们有一个空序列:
-
如果当天 \(A\) 工厂生产了,就在序列末尾加入一个 \((\) 。
-
如果 \(B\) 工厂生产了,就在序列末尾加入一个 \()\) 。
-
如果都生产了,则加入 \(()\) 。
然后我们观察一下最终的序列,发现这一定是一个合法的括号匹配:由于同一张光盘的 \(A\) 工序必须在 \(B\) 工序之前(或同一天),所以最终选出的括号序列必须是一个合法括号序列。将左括号记为 \(+1\),右括号记为 \(−1\),则其前缀和始终非负,且总和为 \(0\)。
那么每次增广相当于在序列中插入一对括号:
-
选择 \(a_i\) 相当于添加一个左括号 \((\)。
-
选择 \(b_j\) 相当于添加一个右括号 \()\)。
增广路有两种形态:
-
不回退边:形如 \(... ( ... ) ...\),即 \(i≤j\)。这是直接配对,无额外限制。
-
回退边:形如 \(... ) ... ( ...\),即 \(i>j\)。这表示“反悔”之前的某个匹配,需要满足在 \(j\) 到 \(i\) 之间的前缀和严格大于 \(0\)(即存在可回退的流量)。
因此,每次增广就是在合法前缀和非负的约束下,寻找一对 \((i,j)\) 使得 \(a_i+b_j\) 最小。
而一次操作会导致一个区间的前缀和发生变化,所以考虑用线段树维护。
Difficulty: 维护线段树
节点定义
每个线段树节点维护以下信息:
| 字段 | 含义 |
|---|---|
mn |
区间前缀和最小值 |
mna |
区间内 \(a_i\) 最小的位置 |
mnb |
区间内 \(b_i\) 最小的位置 |
va |
正向增广最小费用 \(a_i + b_j\),其中 \(i \le j\) |
vai, vaj |
对应正向增广的 \(i, j\) |
vb |
反向增广(受限)最小费用,要求中间前缀和 \(>\) 整个区间最小前缀和 |
vbi, vbj |
对应反向增广的 \(i, j\) |
vc |
反向增广(不受限)最小费用 \(a_i + b_j\),其中 \(i > j\) |
vci, vcj |
对应反向增广的 \(i, j\) |
alim |
满足“前缀和 \(>\) 区间最小前缀和”的后缀中 \(a\) 最小的位置 |
blim |
满足“前缀和 \(>\) 区间最小前缀和”的前缀中 \(b\) 最小的位置 |
tag |
区间加懒标记 |
其中 \(a_i\) 表示在 \(A\) 工厂加工的费用,\(b_i\) 表示在 B 工厂加工的费用。
合并操作(push_up)
设当前节点为 \(T\),左儿子为 \(L\),右儿子为 \(R\)。
- 基础信息
- 正向增广 \(va\)
正向增广要求 \(i \le j\),因此可以完全从左儿子、右儿子或跨越左右儿子得到。
若取到跨越方案,则 \(T.vai = L.mna\),\(T.vaj = R.mnb\)。
- 反向增广(不受限)\(vc\)
反向增广要求 \(i > j\),因此跨越时必须是右儿子的 \(i\) 配左儿子的 \(j\)。
若取到跨越方案,则 \(T.vci = R.mna\),\(T.vcj = L.mnb\)。
- 反向增广(受限)\(vb\) 与 \(alim, blim\)
这是合并的核心,需要根据 \(L.mn\) 与 \(R.mn\) 的大小关系分三种情况讨论。
-
情况一:\(L.mn > R.mn\)
此时整个区间的最小前缀和落在右儿子。
-
左儿子内部的所有反向增广都满足“中间前缀和 \(>\) 全局最小前缀和”,因此可以直接用 \(L.vc\) 更新 \(T.vb\)。
-
跨越方案:\(j\) 在左儿子任选(取 \(b\) 最小的 \(L.mnb\)),\(i\) 在右儿子必须满足前缀和 \(>\) 全局最小前缀和,取 \(R.alim\)。
-
同时:
-
情况二:\(L.mn < R.mn\)
此时整个区间的最小前缀和落在左儿子。
-
右儿子内部的所有反向增广都满足条件,可以直接用 \(R.vc\) 更新 \(T.vb\)。
-
跨越方案:\(j\) 在左儿子必须满足条件,取 \(L.blim\);\(i\) 在右儿子任选,取 \(a\) 最小的 \(R.mna\)。
-
同时:
-
情况三:\(L.mn = R.mn\)
此时整个区间的最小前缀和在左右两边都能取到。
-
继承左右儿子的 \(vb\)。
-
跨越方案:\(j\) 在左儿子必须满足条件,取 \(L.blim\);\(i\) 在右儿子必须满足条件,取 \(R.alim\)。
-
同时:
复杂度
每次合并为 \(O(1)\),线段树单次修改/查询为 \(O(\log n)\)。总增广 \(k\) 次,总复杂度 \(O(k \log n)\)。
注意事项
- 下标从 \(1\) 开始,可增设虚拟节点 \(0\) 并令 \(a[0] = b[0] = +\infty\) 以避免边界判断。
- 所有取最小值操作需同时记录位置下标。
- 懒标记下传时需更新 \(mn\),并保持其他信息不变。

浙公网安备 33010602011771号