首页 > 技术文章 > 斜率优化略解

yzhang-rp-inf 2018-12-27 21:57 原文

斜率优化在dp中是比较常见的优化方法

斜率优化算法最重要的一步,就是要将状态转移方程改写为y=kx+b的形式。而这一算法主要的难点就在于,对于一个复杂的状态转移方程,我们将哪些项将作为y,哪些项最为k和x,哪些项作为b。

我们以Luogu P3195 [HNOI2008]玩具装箱TOY为例来说斜率优化

在本题中,设前缀和为sum[i],由题意易得dp方程:

\[dp[i]=Min(dp[j]+(sum[i]+i-sum[j]-j-L-1)^2) (j<i) \]

这个方程明显是\(O(N^2)\)的,过不了此题

\(a[i]=sum[i]+i\)\(b[i]=sum[i]+i+L+1\)(这一步是为了简化计算)

则(先忽略Min):

$$dp[i]=dp[j]+(a[i]-b[j])^2$$

展开得:

$$dp[i]=dp[j]+a[i]2-2⋅a[i]⋅b[j]+b[j]2$$

移项得(移的项实际比较套路):

$$2⋅a[i]⋅b[j]+dp[i]−a[i]2=dp[j]+b[j]2$$

我们把这个方程转化成y=kx+b的形式:

\(dp[j]+b[j]^2\)是y,\(2⋅a[i]\)是k(斜率),\(b[j]\)是x,\(dp[i]-a[i]^2\)是b(截距)

这个式子珂以变成平面直角坐标系上的一条一次函数

因为对于每个i来说,a[i]都是确定的

所以dp[i]的含义珂以转化为:当上述直线过点\(P(b[j],dp[j]+b[j]^2)\)时,直线在y轴的截距加上\(a[i]^2\)(一个定值)

而题目即为找这个截距的最小值

本题中可能为最优的P点组成了一个下凸包(其他题目可能不同,结合题目具体分析)

显然,凸包中相邻两点斜率是单调递增的

而目标直线的斜率\(2⋅a[i]\)也是单调递增的

满足条件的最优\(P_j\)一定为第一个\(slope(P_j,P_{j+1}) > 2 \cdot a[i]\)的点(slope(a,b)表示a,b两点连线的斜率)

我们珂以考虑用单调队列来维护\(slope(P_j,P_{j+1})\)

完整代码:

#include <bits/stdc++.h>
#define N 50005
#define db double
#define getchar nc 
using namespace std;
inline char nc(){
    static char buf[100000],*p1=buf,*p2=buf; 
    return p1==p2&&(p2=(p1=buf)+fread(buf,1,100000,stdin),p1==p2)?EOF:*p1++; 
}
inline int read()
{
    register int x=0,f=1;register char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9')x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
    return x*f;
}
inline void write(register long long x)
{
    if(!x)putchar('0');if(x<0)x=-x,putchar('-');
    static int sta[35];register int tot=0;
    while(x)sta[tot++]=x%10,x/=10;
    while(tot)putchar(sta[--tot]+48);
}
int n,L;
db sum[N],dp[N];
int head,tail,q[N];
inline db a(register int i)
{
    return sum[i]+i;
}
inline db b(register int i)
{
    return a(i)+L+1;
}
inline db Y(register int i)
{
    return dp[i]+b(i)*b(i);
}
inline db slope(register int i,register int j)
{
    return (Y(i)-Y(j))/(b(i)-b(j));
}
int main()
{
    n=read(),L=read();
    head=tail=1;
    for(register int i=1;i<=n;++i)
    {
        sum[i]=(db)read();
        sum[i]+=sum[i-1];
        while(head<tail&&slope(q[head],q[head+1])<a(i)*2)
            ++head;
        dp[i]=dp[q[head]]+(a(i)-b(q[head]))*(a(i)-b(q[head]));
        while(head<tail&&slope(i,q[tail-1])<slope(q[tail-1],q[tail]))
            --tail;
        q[++tail]=i; 
    }
    write((long long)dp[n]);
    return 0;
}

在这道题中,相邻决策点斜率和每个点的横坐标递增,但在其他题目中还会出现以下情况

一、相邻两个决策点的斜率不递增

这种情况下,我们无法保证小于等于当前斜率的决策点j一定也小于下一个状态的斜率。因此不能进行“弹掉队首元素”这一步。但是这不影响我们维护一个下凸壳,因此我们可以在单调队列(下凸壳)内进行二分查找,找到第一个使该决策点和下一个决策点构成的线段斜率>k的决策点,即为当前最优决策。

二、每个决策点横坐标不递增

这意味着要在凸壳任意位置动态插入顶点、动态查询,我们珂以使用平衡树来维护凸壳。

推荐阅读