模拟费用流

模拟费用流

加速费用流,用数据结构来模拟费用流的过程

例: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\)​ 相当于添加一个右括号 \()\)。

增广路有两种形态:

  1. 不回退边:形如 \(... ( ... ) ...\),即 \(i≤j\)。这是直接配对,无额外限制。

  2. 回退边:形如 \(... ) ... ( ...\),即 \(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\)。

  1. 基础信息

\[T.mn = \min(L.mn, R.mn) \]

\[T.mna = \begin{cases} L.mna, & a[L.mna] \le a[R.mna] \\ R.mna, & \text{otherwise} \end{cases} \]

\[T.mnb = \begin{cases} L.mnb, & b[L.mnb] \le b[R.mnb] \\ R.mnb, & \text{otherwise} \end{cases} \]

  1. 正向增广 \(va\)

正向增广要求 \(i \le j\),因此可以完全从左儿子、右儿子或跨越左右儿子得到。

\[T.va = \min(L.va, R.va, a[L.mna] + b[R.mnb]) \]

若取到跨越方案,则 \(T.vai = L.mna\),\(T.vaj = R.mnb\)。

  1. 反向增广(不受限)\(vc\)

反向增广要求 \(i > j\),因此跨越时必须是右儿子的 \(i\) 配左儿子的 \(j\)。

\[T.vc = \min(L.vc, R.vc, a[R.mna] + b[L.mnb]) \]

若取到跨越方案,则 \(T.vci = R.mna\),\(T.vcj = L.mnb\)。

  1. 反向增广(受限)\(vb\) 与 \(alim, blim\)

这是合并的核心,需要根据 \(L.mn\) 与 \(R.mn\) 的大小关系分三种情况讨论。

  • 情况一:\(L.mn > R.mn\)

    此时整个区间的最小前缀和落在右儿子。

    • 左儿子内部的所有反向增广都满足“中间前缀和 \(>\) 全局最小前缀和”,因此可以直接用 \(L.vc\) 更新 \(T.vb\)。

    • 跨越方案:\(j\) 在左儿子任选(取 \(b\) 最小的 \(L.mnb\)),\(i\) 在右儿子必须满足前缀和 \(>\) 全局最小前缀和,取 \(R.alim\)。

\[T.vb = \min(L.vc, R.vb, b[L.mnb] + a[R.alim]) \]

同时:

\[T.alim = R.alim \]

\[T.blim = L.mnb \]

  • 情况二:\(L.mn < R.mn\)

    此时整个区间的最小前缀和落在左儿子。

    • 右儿子内部的所有反向增广都满足条件,可以直接用 \(R.vc\) 更新 \(T.vb\)。

    • 跨越方案:\(j\) 在左儿子必须满足条件,取 \(L.blim\);\(i\) 在右儿子任选,取 \(a\) 最小的 \(R.mna\)。

\[T.vb = \min(L.vb, R.vc, b[L.blim] + a[R.mna]) \]

同时:

\[T.alim = R.mna \]

\[T.blim = L.blim \]

  • 情况三:\(L.mn = R.mn\)

    此时整个区间的最小前缀和在左右两边都能取到。

    • 继承左右儿子的 \(vb\)。

    • 跨越方案:\(j\) 在左儿子必须满足条件,取 \(L.blim\);\(i\) 在右儿子必须满足条件,取 \(R.alim\)。

\[T.vb = \min(L.vb, R.vb, b[L.blim] + a[R.alim]) \]

同时:

\[T.alim = R.alim \]

\[T.blim = L.blim \]

复杂度

每次合并为 \(O(1)\),线段树单次修改/查询为 \(O(\log n)\)。总增广 \(k\) 次,总复杂度 \(O(k \log n)\)。

注意事项
  • 下标从 \(1\) 开始,可增设虚拟节点 \(0\) 并令 \(a[0] = b[0] = +\infty\) 以避免边界判断。
  • 所有取最小值操作需同时记录位置下标。
  • 懒标记下传时需更新 \(mn\),并保持其他信息不变。
posted @ 2026-09-27 18:43  wlhao  阅读(11)  评论(0)    收藏  举报