slope trick
考虑一个二维的 dp,对其凸性进行研究。
对于某一个 \(f_i\) 而言,建立 \((j,f_{i,j})\) 的函数图像,如果其满足以下性质:
凸性、连续、分段一次函数、斜率为整数且不大。
我们就可以选择 slope trick。注意它和斜率优化并不一样,斜率优化只能优化单个状态转移的复杂度,slope trick 可以优化整个状态转移的复杂度。
考虑对于 \(f_i\),将其视作若干斜率为 \(1\) 的线段(线段长度可以为 \(0\)),用堆维护斜率的变化点,显然一个点可能会被放入多次(那里的显式斜率变化大于 \(1\),长度为 \(0\) 的线段被视为隐性的)。由于函数是凸的,那么对于一些前缀最小值加某个一次函数之类的转移就能比较方便的维护。先来考察一些性质:
-
两个凸函数相加还是凸函数。这个性质同时说明了一次函数加凸函数还是凸函数。进一步地,将“凸函数”换成上文所说的“分段一次凸函数”也是成立的。
-
两个分段一次凸函数相加后的变化点集合为原来两个函数变化点集合的并。
注意到如果要复原一个分段一次凸函数,只维护变化点集合是不够的,还需要维护最左边的点值。这样我们通过一些合并性良好的信息就维护出了一个分段一次凸函数。至于具体怎么优化 dp,还是需要结合题目本身的性质。
Ex1.P4597
一个关键的性质是即使不考虑修改后的数必须在修改前出现过,也会存在一个最优解使得所有修改后的数在修改前出现,感受一下就可以证明。所以令 \(f_{i,j}\) 代表将 \(a_i\) 改成 \(j\),前 \(i\) 个数的最小代价,时间复杂度 \(O(n\max a_i)\),太菜了。
首先,我们有结论,\(f_i\) 是下凸的。证明考虑归纳,\(f_1\) 显然成立,否则写出转移形式 \(f_{i,j}=\min\limits_{k\le j}f_{i-1,j}+|a_i-j|\),考虑 \(f_{i-1}\) 的函数图像,由归纳知其下凸,将这个下凸函数的上升段全部删去替换为顶点的值,这样转移形式就是该函数加上一个绝对值函数,根据两个下凸函数加起来还是下凸函数可以得到 \(f_i\) 下凸。
不过我们还是需要考虑加上绝对值函数后原函数到底有什么变化,根据上文斜率变化点是两个原函数斜率变化点的并的结论,可以对 \(f_{i-1}\) 的每一段分别考虑。对于所有在绝对值函数顶点左侧的线段,斜率 \(-1\),证明考虑纵坐标的变化就是减去横坐标,列式子可以得到此结论。类似地,右侧的线段斜率 \(+1\)。
上文讨论的都是将下凸函数上升段全部替换掉以后的场景,套用到替换前的场景,此时讨论的情况就有四种(斜率正负、顶点左右),斜率为负的情况上文已经讨论过了,对于斜率为正的情况,若在顶点左侧则斜率变为 \(-1\),否则变为 \(1\)(注意不是加减)。
这个过程中只有 \(O(n)\) 个变化点,直接维护变化点可以做到 \(O(n^2)\)。这是一个了不起的进步。考虑进一步利用函数的性质。实际上我们不关心斜率为正的部分,看上去无论如何它们都无法影响到答案。考虑一次转移前的顶点 \(op\),需要将其加上 \(f(x)=|a_i-x|\) 这个函数,若顶点(即 \(a_i\))在 \(op\) 右侧,此时 \(a_i\) 就会成为新的顶点,同时前面的所有变化点维护的线段斜率都会 \(-1\),我们用堆来维护这个过程,一个斜率对应的变化点为弹出该斜率的相反数次后堆顶的元素(或者可以维护线段右端点,定义对应修改)。这样只需要将 \(a_i\) 插入堆即可,不用插入多次的原因比较显然,此处略去。若 \(a_i\) 在 \(op\) 左侧,此时意味着 \(a_i\) 右边的斜率都要 \(+1\),上文说过我们不关心斜率为正的线段,因此将堆顶弹出即可,注意需要插入两个 \(a_i\),考虑修改后 \(a_i\) 左侧线段斜率 \(-1\),右侧 \(+1\),因此需要插入两个 \(a_i\)。
考虑如何在上述操作中维护答案的变化,若直接插入则意味着此时顶点处的转移为 \(f_{i,a_i}=f_{j,op}+0\),因此答案不变。否则记插入前顶点为 \(op_{i-1}\),插入后为 \(op_i\),则插入后 \(f_{i,op_i}=f_{i,op_{i-1}}\),证明考虑两者转移后连线斜率为 \(0\),因此答案加上 \(op_{i-1}-a_i\) 即可,此处 \(op_{i-1}\) 就是堆顶的元素。时间复杂度 \(O(n\log n)\),代码及其简单。
使用 slope trick 的时候,需要固定好维护的是左端点还是右端点,才能对转移做出准确的判断。