斜率优化dp

· · 个人记录

之前我们学习过,对于dp方程 f[i]=min/max{f[j]+val(i,j)} 可以进行优化。如果多项式val(i,j)每一项仅与i/j中的一个有关,那么就可以利用单调队列进行优化。那么,如果val(i,j)中含有i、j的乘积项,那么就可以利用斜率进行优化。

经做题发现,斜率优化的一般使用范围为:\color{red}\texttt{一个序列在不改变顺序的情况下找到最优分块方式。}

那么,先看道具体例题来讲:

P3195 [HNOI2008]玩具装箱

首先,这道题的样子长得很标准:一个序列在不能改变顺序的情况下找到分块方式使得花费最小。那么,根据题意,设f[i]表示到第i个玩具最少的花费,可以列出最朴素的dp方程式为:

\color{blue}f[i]=min(f[j]+(j-i+sum[i]-sum[j]-l-1)^2)

此时,方程式O(n^2)的,时间复杂度太高,接下来思考怎样进行优化。

观察方程式,满足上述形式,再观察val(i,j)的部分,发现展开后肯定有i、j的乘积项,这时候就要思考进行斜率优化。

斜率优化是利用数形结合的思想,利用决策单调性,及时排除无用决策点而得以进行优化。

首先将原方程式的平方拆开并进行化简。为避免项太多,不妨把i、j的参数分别换元。设s[i]=sum[i]+i ,那么s[j]=sum[j]+j,同时令ll=l+1,替换后将平方拆开,方程式就可以化简为:

\large\color{red}f[i]=f[j]+s[i]^2+(s[j]+ll)^2-2 * s[i]* s[j] - 2* s[i]* ll

斜率优化有两种常见的方式:

方法一:

大体思想为:对于一个点i,其最优决策点具有单调性。把i、j放在平面直角坐标系中会看得更清晰。

首先,将表达式化简为一个函数解析式的样子。其中,j为未知数,即y和x,i为已知数,即为k和b,那么,将只含有j的项移到左边当成y,将i、j的乘积项放在右边当做kx,将只含有i的项放在最后当做b。那么式子转化为:

\large\color{red}f[j]+(s[j]+ll)^2 = 2 * s[i] * s[j] +f[i]+2* s[i]* ll+s[i]^2

\huge\color{blue}y =kx+b

这样,函数解析式就与表达式一一对应了。

此时,对于每一个i,都会有一条新的斜率为2* s[i] 的直线去寻找最优的决策点j,且所求的f[i]在b中,而2* s[i]* ll+s[i]^2为定值,所以f[i]的值仅与b的值有关,且b的值越小f[i]的值就越小。怎样让b取到最小值呢?再回到解析式中,k是一定的那么对于许多决策点j,当直线从下到上平移碰到第一个决策点j,那么此时与y轴的交点就是最小值。

画个图理解一下:

一条直线由下到上平移直到碰到第一个决策点。

那么最终所有决策点就形成了这样的形状

比如说这样,每一个黑点都代表一个决策点,每一条直线是以2* s[i]为斜率的,与众多决策点的最低交点即为所求。对于数量有限的直线,其最优决策点只有部分,且决策是具有单调性的,所以只留 下凸包 即可。

怎样保留呢?类似于单调队列的方法,维护一个队头即为最优决策点,如果队头与队头+1的点所连线段的斜率是小于当前直线解析式的,那么,对于直线来说队头+1的点肯定更优,那么队头就没有必要保留了。

当进行插入操作时,如果队尾元素与队尾元素-1 的两点所连线段斜率大于队尾元素与i所连线段,那么队尾是没有意义的,删去即可。

最后,求解斜率时,就是(Y(i)-Y(j))/(X(i)-X(j)),回看解析式,Y(i)就是f[j]+(s[j]+ll)^2X(i)就是s[j].

这里,因为斜率是个小数,除时容易丧失精度,所以要开long double,这样精度还是可以接受的。

方法二

这种方法是直接进行斜率的比较。

对于两个决策点j_{1} >j_{2} ,若要使j_1的f[i]更优,那么必须满足:

\color{red}f[j_1]+s[i]^2+(s[j_1]+ll)^2-2 * s[i]* s[j_1] - 2* s[i]* ll<f[j_2]+s[i]^2+(s[j_2]+ll)^2-2 * s[i]* s[j_2] - 2* s[i]* ll

左右两边化简,约去含I的项,剩下:

\color{red}f[j_1]+(s[j_1]+ll)^2-2 * s[i]* s[j_1] <f[j_2]+(s[j_2]+ll)^2-2 * s[i]* s[j_2]

此时,继续化简,将斜率试着表示出来,原则是将只含j的放到一端,含i的放到另一端。

式子变成了:

\color{red}f[j_1]+(s[j_1]+ll)^2-f[j_2]-(s[j_2]+ll)^2 <2 * s[i]* s[j_1]-2 * s[i]* s[j_2]

进一步化简成:

\color{red}f[j_1]+(s[j_1]+ll)^2-f[j_2]-(s[j_2]+ll)^2<2* s[i]* (s[j_1]-s[j_2])

再把含j的除过去,变成

\color{red}\frac{f[j_1]+(s[j_1]+ll)^2-f[j_2]-(s[j_2]+ll)^2}{ s[j_1]-s[j_2] }<2 * s[i]

式子左边就是j_1j_2所连成线段的斜率,只要满足比2* s[i]小,那么决策点j1就是无用的,就可以从队列中剔除了。

把这两个图搬过来对照式子再理解一下~

最后再附上完整的代码:

#include<bits/stdc++.h>
using namespace std;
int n,q[500005];
long long f[500005],a[500005],l,sum[500005];
long long Y(int j){
    return f[j]+(sum[j]+l)*(sum[j]+l);
}
long long X(int j){
    return sum[j];
}
long double slope(int i,int j){
    return (long double)(Y(j)-Y(i))/(X(j)-X(i));//计算斜率,一定要用long double 
}
int main(){
    cin>>n>>l;
    l++;
    for(int i=1;i<=n;i++){
        cin>>a[i];
    }
    for(int i=1;i<=n;i++){
        sum[i]=sum[i-1]+a[i]+1;//计算前缀和 
    }
    int hd=1,tl=1;
    q[hd]=0;
    for(int i=1;i<=n;i++){
        while(hd<tl&&slope(q[hd],q[hd+1])<=2*sum[i])hd++;//剔除无用队头——!!!这里需要注意:因为需要保证队列里至少留两个点形成线段,所以hd<tl,不能取等号 
        f[i]=f[q[hd]]+(sum[i]-sum[q[hd]]-l)*(sum[i]-sum[q[hd]]-l);//更新f[i] 
        while(hd<tl&&slope(q[tl-1],q[tl])>=slope(q[tl-1],i))tl--;//剔除无用队尾元素(ps:据观察,这里的大小比较一般与上面的符号是相反的) 
        q[++tl]=i;//入队操作 
    }
    cout<<f[n];
    return 0;
}

<补>决策单调性证明

因为斜率k=2* s[i],且随着i的增大s[i]也不断增大,那么直线的斜率就会不断增大,那么,从图上可以看出,最右决策点在不断右移,满足决策的单调性.