【学习笔记】Slope Trick

Slope Trick

CF上讲解Slope Trick的博客

Slope Trick 是一种优化 DP 的方法,核心思想是通过存储DP转移的一些关键信息(如分段函数的分段点和最左/右边的一段函数)从而利用数据结构高效维护转移。(不是斜率优化DP

接下来都以凸函数,维护最左边的函数为例

注:图中所画斜率有小于 \(1\) 的只是为了好看,实际题目的斜率一定为整数

一般可以使用 Slope Trick 优化的DP方程有以下要求:

  • 是连续的
  • 是分段一次函数
  • 是凸函数(斜率单调不升,图像上凸)或凹函数

分段一次凸函数具有非常优秀的性质:

  • 两个凸函数相加还是凸函数
    设凸函数 \(F(i)\)\(G(i)\),其分段点集合分别为 \(L_F\)\(L_G\),其最右端的函数分别为 \(R_F(i)\)\(R_G(i)\),则 \(H(i)=F(i)+G(i)\) 也是凸函数,分段点集合满足 \(L_H=L_F \cup L_G\)\(R_H(i)=R_F(i)+R_G(i)\)

  • 凸函数加一次函数还是凸函数

这样我们将所有斜率减少(增加)\(1\)位置放到数据结构里面维护,也就是说如果一个地方的斜率变化大于 \(1\) 就加多次。

如函数

\[\begin{cases} x+2 & x< -1\\ 1 &-1\le x <1\ \\ -2x+3 & x \ge 1 \end{cases}\]

图像为

image

我们维护最左边的图像信息 \(k=1,b=2\) 和分段点集合 \(\{-1,1,1\}\),其中 \(x=1\) 这个位置加了两次

再如绝对值函数(凹函数) \(|x-w|\),维护 \(k=-1,b=w\) 和分段点集合 \(\{w,w\}\)

事实上每一个分段一次凸函数都可以这样表示,我们就可以快速维护许多函数操作。

几种常见的函数操作:

  • 相加
    直接最左边图像 \(k,b\) 相加,分段点集合合并。

  • 找最大/小值
    就是斜率为零的那一段,用大根堆 \(L\) 维护其左边的所有分段点,\(R\) 维护右边的,始终保证 \(k=L.size()\),其余点扔进 \(L\)
    堆顶即为最大值的左端点和右端点。

  • 加一次函数
    例如加上一个 \(y=x\),就只用把 \(R\) 堆顶的第一个元素扔到 \(L\) 里面
    如果加的一次函数斜率较大,为了保证复杂度,修改集合的定义为每个位置的斜率变化量即前/后缀差分,修改 \(k,b\) 即可。

  • 前后缀max
    前缀 \(\max\) 就是直接扔掉 \(R\) 里的所有点,后缀就是扔掉 \(L\) 里的所有点。

  • 平移
    只用维护 \(k,b\) 的变化,分段点打平移标记。

  • 翻转
    只用维护 \(k,b\) 的变化,分段点打翻转标记。

答案统计的时候有两种方法,一种是还原图像,另一种是记录决策点(一般是斜率为零的线段端点)

例题:CF713C

给定一个有 \(n\) 个正整数的数组,一次操作中,可以把任意一个元素加一或减一。(元素可被减至负数或 \(0\)),求使得原序列严格递增的求最小操作次数。

首先把严格递增转化为单调不降,只需令 \(a_i-=i\),然后就是线性 DP

\[f_{i,j}=\min_{k=1}^jf(i-1,k)+|a_i-j| \]

\(F_i(x)=f_{i,x}\)\(G_i(x)=\min\limits_{k=1}^xF_i(x)\),改写成函数的形式,则转移方程就是

\[F_i(x)=G_{i-1}(x)+|a_i-x| \]

发现这是个斜率单调不降的凹函数:

首先 \(F_1(x)=|a_i-x|\) 这是个凹函数,然后 \(G_i(x)\) 是个前缀 \(min\)

凹函数加凹函数,所以 \(F_i(x)\) 也是凹函数。

所以状态转移其实就是维护凹函数相加和取前缀 \(\min\) 两个操作,这很 Slope Trick,发现 \(G_i(x)\) 的图像最后一段一定是平的,所以我们只需要维护斜率小于零的大根堆 \(L\)

加入绝对值函数 \(|a_i-x|\),相当于在集合中加入元素 \(\{a_i,a_i\}\),分类讨论 \(a_i\) 的位置:

  • \(a_i \ge L.top()\) 时,加的第二个 \(a_i\) 实际斜率是 \(1\),取前缀 \(\min\) 时又删除了,所以此时只用加一个 \(\{a_i\}\)
  • \(a_i < L.top()\) 时,先加入 \(\{a_i,a_i\}\)\(L.top()\) 的斜率从 \(0\) 变成 \(1\),取前缀 \(\min\) 时需要删除,所以 \(pop\) 出去。

这样我们同时完成了凹函数相加和取前缀 \(\min\) 操作。

这道题有个很好的性质就是你每次决策点就是堆顶,所以直接累加答案,这道题就做完了。

四倍经验:CF713CCF13CP2893P4597,注意是严格非降还是严格上升
Code:

priority_queue<int> q;
main(){
    int ans=0;
    int n;cin>>n;
    for(int i=1;i<=n;i++){
        int x;
        cin>>x;
        x=x-i;
        q.push(x);
        if(x<q.top()){
            q.push(x);
            ans+=q.top()-x;
            q.pop();
        }
    }
    cout<<ans<<endl;
}
posted @ 2023-10-05 07:36  CCComfy  阅读(687)  评论(0编辑  收藏  举报