【学习笔记】Slope Trick
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\) 就加多次。
如函数
图像为
我们维护最左边的图像信息 \(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(x)=f_{i,x}\),\(G_i(x)=\min\limits_{k=1}^xF_i(x)\),改写成函数的形式,则转移方程就是
发现这是个斜率单调不降的凹函数:
首先 \(F_1(x)=|a_i-x|\) 这是个凹函数,然后 \(G_i(x)\) 是个前缀 \(min\)
![](https://img2023.cnblogs.com/blog/3091262/202310/3091262-20231005160920901-82953079.png)
凹函数加凹函数,所以 \(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\}\)。
![](https://img2023.cnblogs.com/blog/3091262/202310/3091262-20231005170026969-1418525805.png)
- 当 \(a_i < L.top()\) 时,先加入 \(\{a_i,a_i\}\),\(L.top()\) 的斜率从 \(0\) 变成 \(1\),取前缀 \(\min\) 时需要删除,所以 \(pop\) 出去。
![](https://img2023.cnblogs.com/blog/3091262/202310/3091262-20231005171139809-1588545633.png)
这样我们同时完成了凹函数相加和取前缀 \(\min\) 操作。
这道题有个很好的性质就是你每次决策点就是堆顶,所以直接累加答案,这道题就做完了。
四倍经验:CF713C,CF13C,P2893,P4597,注意是严格非降还是严格上升
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;
}
本文来自博客园,作者:CCComfy,转载请注明原文链接:https://www.cnblogs.com/cccomfy/p/17743031.html