Slope Trick 是二维 dp 的优化方法,用于优化一个凹/凸函数。

我们利用分段点集合来表示这个函数。比如一个凹函数,−2,2,2{-2,2,2} 表示在 −2-2 的时候函数斜率 −1-1,在 22 的时候 −2-2,然后还知道最后 k=2,b=−6k=2,b=-6。

  • 相加:k,bk,b 相加,集合合并。

  • 找最小值:找到斜率为 00 的位置。具体的方法就是我们用大根堆维护左边的,用小根堆维护右边的,保证小根堆大小大小为原始的 kk 就行了。

  • 前缀/后缀 min⁡\min:直接清空大根堆/小根堆堆。

P4597 序列 sequence

P4597 序列 sequence。

20242024 年做这道题的时候,并不知道什么 Slope Trick,只是觉得这是一个技巧,可以水过好几倍经验。

fi,jf_{i,j} 表示处理到 ii,把它改成 jj 的最小操作次数。

fi,j=min⁡k≤jfi−1,k+∣ai−j∣f_{i,j} = \min_{k \le j} f_{i-1,k} + |a_i-j|

fi(x)f_i(x) 是一个凹函数。因为 i=1i=1 显然,i>1i>1时,两个函数相加仍然是。

我们每一次输入 aia_i 都会让 aia_i 的斜率增加 22。然后每一次都需要 pop 来保证维护的是斜率最小的点,每一次的贡献就是最小值的点和当前的差的绝对值。

代码
C++
for (int i = 1; i <= n; i++)
    {
        int x; cin >> x;
        q.push(x); q.push(x);
        ans += q.top() - x;
        q.pop();
    }

P3642 [APIO2016] 烟花表演

P3642 [APIO2016] 烟花表演。

设 gi,jg_{i,j} 表示在 ii 这个点,所有儿子节点需要 jj 秒引爆的代价。这是一个凹函数。

然后我们还需要一个 fi,jf_{i,j} 表示 ii 这个子树所有节点在 jj 秒引爆的代价。

设 gxg_x 的最优区间为 [L,R][L,R]。

fi(x)={gx+wx≤L需要把这条边距离缩短为 0gL+l−(x−L)L<x≤L+lL 时间引爆需要修改的路径长度gLL+l<x≤R+l存在刚好能走完后恰到 x 的出发时间gL+x−R−lR+l<x需要延长的路径长度f_i(x)= \begin{cases} g_x + w & x\le L&\text{需要把这条边距离缩短为 0}\\ g_L + l-(x-L) & L<x\le L+l&\text{L 时间引爆需要修改的路径长度}\\ g_L&L+l<x\le R+l &\text{存在刚好能走完后恰到 x 的出发时间}\\ g_L + x-R-l&R+l<x&需要延长的路径长度 \end{cases}

其实后面的我也不知道怎么去想到,或许我们只是想优化一下转移。

  • 第一种是直接将 gxg_x 的图像向上平移 ll。
  • 第二种是插入 fx=−x+gL+L+lf_x = -x + g_L +L + l 这条斜率为 −1-1 的线。
  • 第三种是把原来的 [l,r][l,r] 向右平移了 ll。
  • 第四种是插入 fx=x+gL+x−l−Rf_x = x + g_L + x - l - R 这条斜率为 11 的直线。

接下来就用 Slope Trick 来维护了,用一个大根堆来维护。

再加上这个函数是连续的这个性质...

  • 第一种是直接将 f0f_0 加上 ww。
  • 第二种和第三种直接弹出 L,RL,R 然后再塞入 L+l,R+lL+l,R+l,此时斜率为 −1-1 的线段被延长,然后 [L+l,R+l][L+l,R+l] 的斜率为 00。
  • 第四种是合并完所有子节点的堆之后弹出 子节点个数−1子节点个数-1 个最大的点。

最后的答案就直接在根节点的堆里面,用 f0f_0 减去所有拐点坐标就行了。