学习笔记——斜率优化 dp
引入
斜率优化,是单调队列优化的一个进阶版本,为了更好地理解,先来回顾一下单调队列吧~
所谓单调队列优化,就是对于形如:
的 \(dp\) 式,我们把所有的 \(dp_j+a_j\) 放进单调队列里,实现 \(O(1)\) 的转移。
这个时候,只有在式子中,每一项只有关于 \(j\) 或者只有关于 \(i\) 的。但是如果有这样一个式子:
这时候,我们就不能只考虑 \(a_j\),因为对于每个 \(i\),其大小都是不同的。
流程
我们先来看一道例题:[HNOI2008]玩具装箱
我们很容易得到原始的 \(dp\) 式子,\(c_i\) 是前缀和:
为了简化,我们设 \(a_i=c_i+i\),\(b_i=c_i+i+1+L\)。
则可以转化为:
我们可以发现其中有一项 \(2a_ib_j\),不能用单调队列。
这时候,我先把式子移项成:
其中 \(j\) 项都是已知的,我们需要从中找出使得 \(dp_i\) 最小的。
不妨,我们把 \(b_j\) 看成 \(x\),\(dp_j+b_j^2\) 看成 \(y\)。那么这个式子可以看成是一条直线,而其斜率是固定的 \(2a_i\),截距就是 \(dp_i-a_i^2\)。并且,这条直线必然经过点 \((b_j,dp_j+b_j^2)\)。这时候,我们回来考虑我们的目的:求使得 \(dp_i\) 最小。这也就是说要使截距最小,而斜率是确定的,我们可以看成是一条直线可以上下平移。
如果我们把之前已经求出的化成一个个点,其横坐标是 \(b_j\),其纵坐标是 \(dp_j+b_j^2\),此时只要这条直线经过了这个点,那么也就是说当前的截距加上 \(a_i^2\) 就是从当前的 \(j\) 转移所得到的 \(dp_i\)。为了使得 \(dp_i\) 最小,我们找到从下往上第一个经过这条直线的点转移即可。
有点绕,我们来看几张图:
假如说我们把之前已经得到的数据映射成点是酱紫的:
然后我们用当前的直线从下往上移,当碰到第一个点时就停下转移:
此时得到的 \(dp_i\) 是最小的。
所以我们只要维护一个向下的凸包就可以了:
然后我们发现,斜率 \(2a_i\) 是单调递增的,那么一个点一旦不满足,那么对于之后的直线也一定不会选它了,我们可以用单调队列来维护斜率。这样,我们也可以用类似于单调队列的方式来维护斜率,实现 \(O(1)\) 转移。
接下来,我分享我的套路:
- 写出原始的 \(dp\) 式子;
- 然后把只含有 \(j\) 项的放在右边,其他都移到左边;
- 接下来把既含有 \(i\) 又含有 \(j\) 的项中的 \(j\) 项看成 \(x\),右边看成 \(y\);
- 然后观察斜率是否满足单调性,若满足,则可以用单调队列,否则用二分;
- 最后考虑维护上凸还是下凸。
好了。
Code
#include<bits/stdc++.h>
#define inf 1<<30
#define INF 1ll<<60
#define ll long long
using namespace std;
const int MAXN=5e5+10;
ll a[MAXN],b[MAXN],c[MAXN],dp[MAXN];
int q[MAXN];
ll X(int i){return 2*b[i];}
ll Y(int i){return dp[i]+b[i]*b[i];}
int main()
{
int n,L;
scanf("%d%d",&n,&L);
for(int i=1;i<=n;i++){
scanf("%lld",&c[i]);
c[i]+=c[i-1];
a[i]=c[i]+i;b[i]=c[i]+i+1+L;
}
int head=0,tail=0;
b[0]=L+1;
for(int i=1;i<=n;i++){
while(head<tail&&a[i]*(X(q[head+1])-X(q[head]))>(Y(q[head+1])-Y(q[head])))
head++;
int j=q[head];
dp[i]=dp[j]+(a[i]-b[j])*(a[i]-b[j]);
while(head<tail&&(Y(i)-Y(q[tail]))*(X(q[tail])-X(q[tail-1]))<(Y(q[tail])-Y(q[tail-1]))*(X(i)-X(q[tail])))
tail--;
q[++tail]=i;
}
printf("%lld\n",dp[n]);
}
/*ZMatrisutedX
dp[i]=dp[j]+(c[i]+i-c[j]-j-L)^2
a[i]->c[i]+i b[i]->c[i]+i+L
2*b[j]*a[i]+dp[i]-a[i]^2=dp[j]+b[j]^2
*/