P4694 Raper 题解 = CF802O
算法:线段树维护,模拟费用流。
你需要生产 \(k\) 张光盘。每张光盘都要经过两道工序:先在 A 工厂进行挤压,再送到 B 工厂涂上反光层。
你知道每天 A、B 工厂分别加工一张光盘的花费。你现在有 \(n\) 天时间,每天可以先送一张光盘到 A 工厂(或者不送),然后再送一张已经在 A 工厂加工过的光盘到 B 工厂(或者不送),每家工厂一天只能对一张光盘进行操作,同一张光盘在一天内生产出来是允许的。我们假定将未加工的或半成品的光盘保存起来不需要费用。
求生产出 \(k\) 张光盘的最小花费。
\(k,n\le 5e5\)。
建出费用流模型:把每个光盘视作在每一天之间流动。把每一天抽象出一个点 \(d_1\sim d_n\),\(S\) 向 \(d_i\) 连边(容量 \(1\) 费用 \(a_i\)),\(d_i\) 向 \(T\) 连边(容量 \(1\) 费用 \(b_i\))。\(d_i\) 向 \(d_{i+1}\) 连边(容量 \(+\infty\) 费用 \(0\))。
注意这题还有要求容量限制,所以 \(U\) 向 \(S\) 连容量 \(k\) 费用 \(0\) 的边。
直接费用流必挂。考虑模拟费用流。
我们发现:如果把流了一条 \(a_i\) 边看作添加一个 (
;流了一条 \(b_i\) 边看作添加一个 )
。一次增广路必然是在两个地方插入了一个 (
和一个 )
。
而且最终的括号序列一定顺次形成一个合法括号序列。
既然涉及到括号序列,就考虑 \(\pm 1\) 的前缀和。把 (
看作 \(+1\),)
看作 \(-1\)。下面把前缀和记作 \(s\)。
增广路一共分两种:一种是不回退边的,即 \(a_i\) 的边在 \(b_i\) 的边之前,翻译成括号序列就是 ...(...)...
;另一种就是 ...)...(...
。
不回退边的显然没有任何限制(因为 \(d_i\rightarrow d_{i+1}\) 的容量为 \(+\infty\))。而回退边的要保证在 )
和 (
之间的 .
都是能回退的,也就是之前有用过的。
能回退是什么意思?考虑前缀和的话,就是这一段的 \(s>0\)。
所以我们现在的目标是:分别找到这两类增广路的 \(a_i+b_j\) 最小值,然后选取其中较小的应用。
不妨记增广路增广路 \(a_i\) 和 \(b_j\)。两类增广路分别要求:① \(i\le j\);② \(i>j\) 且 \(\min\{s_j\sim s_i\}>0\)。
应用增广路:① 会使 \(s_{i\sim j}+1\);②会使 \(s_{j\sim i}-1\)。
尝试用线段树维护 \(s\) 和 \(a_i+b_j\) 的最小值,这样可以快速用线段树找到 \(i,j\),进一步找到 \(a_i+b_j\) 将其累加进答案,然后在线段树上修改 \(s_{i\sim j}\pm 1\),之后再找下一次的 \(a_i+b_j\),再再找 …… 如此找 \(k\) 次就能求出答案。
问题变成如何维护一颗线段树支持这些东西。考虑每个结点要维护什么信息。
记这个结点对应的区间是 \([lx,rx]\)。
一个很显然的东西是要维护两个值 \(\min_a,\min_b\),其中 \(\min_a\) 是使 \(a_x\) 最小的 \(x\),\(\min_b\) 是使 \(b_x\) 最小的 \(x\)。
这里有一个很巧妙的转化:因为我们每次都只询问整个区间(根结点)的信息,而 \(s_n\) 恒为 \(0\),所以 \(>0\) 可以转化为 \(>\) 对应区间的最小值。
于是我们在结点上维护一个 \(mn=\displaystyle\min_{i=lx}^{rx}\{s_i\}\)。
不要忘了我们的初心是找增广路。所以在结点上还要维护两个 pair:\(ans1\) 和 \(ans2\),每一个 pair 都包含两个数 \(i,j\)。
\(ans1\) 是 ① 型增广路的最优决策,也就是满足 \(i\le j\) 且 \(a_i+b_j\) 最小的一组 \((i,j)\)。
\(ans2\) 的 \((i,j)\) 是满足 \(i>j\) 且 \(\min\{s_{i\sim j}\}>mn\) 的情况下,\(a_i+b_j\) 最小的一组 \((i,j)\)。
根据直觉,还要再维护一个 \(ans3\):\(ans3\) 的 \((i,j)\) 是满足 \(i>j\) 时 \(a_i+b_j\) 最小的一组 \((i,j)\)。也就是不考虑 \(s\) 的限制的 \(ans2\)。
但这样还是无法更新,缺少信息。(然后就发挥想象力了)
维护一个 \(alim\):\(alim\) 是一个使得 \(\min\{s_{lx\sim alim-1}\}>mn\) 的数,且满足在所有这类数的 \(a\) 中 \(a_{alim}\) 是最小的。
维护一个 \(blim\):\(blim\) 是一个使得 \(\min\{s_{blim\sim rx}\}>mn\) 的数,且满足在所有这类数的 \(b\) 中 \(b_{blim}\) 是最小的。
综上,我们在一个结点上维护了 \(mn,\min_a,\min_b,ans1,ans2,ans3,alim,blim\) 这些东西。(好多)
当然,上面提到的所有都是在 val 上存的值。结点还有 tag,但是 tag 很简单,tag 只涉及对 \(s\) 的区间加(减)。
接下来就考虑 val 和 tag 的运算。
-
tag * tag。简单,相加即可。
-
val * tag。难度稍有上升。考虑使一个区间的 \(s\) 加减会导致什么,显然 \(mn\) 会加减对应的数。但其他的 \(\min_a,\min_b\) 等等,都是一个相对于 \(mn\) 的定义,而加减是整体的加减,所以除了 \(mn\) 都不会变。
-
val + val,即 pushup。线段树维护最难的部分。
考虑如何用两个子结点推出本身的信息。记左儿子为 \(ls\),右儿子为 \(rs\)。
-
\(mn\),这个很简单,就是两个子结点的 \(mn\) 取较小。
就这个简单了 -
\(\min_a,\min_b\),也很简单。比较一下两个子结点哪个 \(\min_a\) 的 \(a\) 更小,对应更新即可。\(\min_b\) 同理。
-
\(ans1\),这个是没有限制的。整个结点的 \(ans1\) 有三种可能:等于左儿子的 \(ans1\),或等于右儿子的 \(ans1\),或由左右儿子共同组合。
我们在这三种情况中取最优即可。前两种都很简单,是 \(ls.ans1\) 和 \(rs.ans1\);看左右儿子共同组合的情况。这种情况显然只有一个最优:\((ls.\min_a,rs.\min_b)\)。
-
\(ans3\),这个同样是没有限制的。和 \(ans1\) 类似,不过左右儿子组合的最优是 \((ls.\min_b,rs.\min_a)\)。
-
更新 \(ans2,alim,blim\)。这些放在一起,是因为都需要根据 \(ls.mn\) 和 \(rs.mn\) 的大小分类讨论。
但是在分类讨论之前,有一点比较显然:可以用 \(ls.ans2\) 和 \(rs.ans2\) 尝试更新本身的 \(ans2\)。
开始分讨。时刻清楚意识到我们在分讨内部要做的事:更新 \(alim,blim\),更新 \(ans2\)。
-
当 \(ls.mn>rs.mn\)。
先考虑非左右儿子组合的。
因为 \(ls.mn>rs.mn\),所以 \(ls.ans3\) 的 \((i,j)\) 无论怎么选,在 \(rs\) 中总有更小的 \(s\),因此我们可以用 \(ls.ans3\) 更新本身的 \(ans2\)。
再考虑左右儿子共同组合的可能性。
即我们要在 \(rs\) 中选出一个 \(i\),在 \(ls\) 中选出一个 \(j\)。取 \(mid=\dfrac{lx+rx}{2}\),\((i,j)\) 要满足 \(\min(\min\{s_{i\sim mid}\},\min\{s_{mid\sim j}\})>\min\{s_{lx\sim rx}\}\)。
这个式子可以拆成:\(\min\{s_{j\sim mid}\}>\min\{s_{lx\sim rx}\}\) 和 \(\min\{s_{mid\sim i}\}>\min\{s_{lx\sim rx}\}\)。
上面说过,\(ls\) 的部分一定大于整体的 \(mn\),所以前者的限制略去。我们对 \(j\) 没有限制了,显然我们会选择 \(j=ls.\min_b\)。
又 \(\min\{s_{lx\sim rx}\}=\min\{s_{mid\sim rx}\}\),所以后者的限制改写为 \(\min\{s_{mid\sim i}\}>\min\{s_{mid\sim rx}\}\)。在这个限制下使 \(a_i\) 最小,显然我们应该取 \(i=rs.alim\)。注意 \(alim\) 表示的是可行前缀中最优的。
好,截至这里。我们在这种情况下只剩更新 \(alim\) 和 \(blim\) 了。
考虑 \(alim\),时刻铭记 \(alim\) 表示的是可行的前缀中最优的。因为 \(ls\) 的任何一个 \(s\) 都大于整体的 \(mn\),所以 \(alim\) 如果最终的在 \(ls\) 中选,一定会选 \(ls.\min_a\);但因为 \(ls\) 可以全部跨过去,所以还有一种可能是 \(rs.alim\)。
\(blim\) 就没有选择了,只能选 \(rs.blim\),没办法跨越全部。
至此,我们完成了一种情况的分讨,还剩两种。 -
当 \(ls.mn<rs.mn\)。与上一种相似,太好了,我们可以跳过。
-
当 \(ls.mn=rs.mn\)。此时最小值在两边都能取到,意味着任何 \((i,j)\) 不能完全覆盖一边。因此更新 \(ans2\) 只能用 \(ls.blim\) 和 \(rs.alim\)(左边的后缀和右边的前缀拼接)。\(alim=ls.alim\),\(blim=rs.blim\)。
-
-
建立这么一颗线段树,每次查询对应的 \(i,j\),\(ans+\!\!=a_i+b_j\),然后 \(a_i,b_j\leftarrow +\infty\)。执行 \(k\) 次。输出 \(ans\)。
终于做完啦!—— 吗?
直接这么写就错了!!!
这里有一个坑,当修改了 \(a_i,b_j\) 的时候,一定要把所有涉及到 \(a_i,b_j\) 的线段树结点都一路 pushdown 下去,再 pushup 回来!
还有一个在代码实现细节上的小技巧:当我们更新 \(ans1,ans2,ans3\) 的时候,显然是需要比较两个 pair 哪个更优的,比较的条件是 \(a_i+b_j\) 哪个更小。那么 \(ans1,ans2,ans3\) 的单位元(ide)怎么设置呢?怎么才能保证无解的情况和任何的合法情况比较都只会留下合法的呢?
可以额外设置 \(a_0=b_0=+\infty\),无解的情况令 pair = \((0,0)\) 即可。