一叶浮萍归大海

人生何处不相逢

关于动态规划的斜率优化

其实关于数形结合的这种思想我一直不太明白
最近学了一下关于斜率优化方面的知识,才慢慢地理解了这种思想方法

使用条件

关于动态规划
如果方程形如:

\[F[i]=min(a[i]*f[j]+b[i]*c[j]+d[i]) \]

就是在方程中有关于i的常数和关于j的常数的乘积。这时候使用斜率优化最恰当。

主要思想

数形结合!!
我们观察式子:
我们要使得决策点j优于决策点k
那就是

\[a[i]*f[j]+b[i]*c[j]+d[i]<a[i]*f[k]+b[i]*c[j]+d[i] \]

\[a[i]*f[j]+b[i]*c[j]<a[i]*f[k]+b[i]*c[k] \]

\[a[i]*f[j]-a[i]*f[k]<b[i]*c[k]-b[i]*c[j] \]

\[a[i]*(f[j]-f[k])<b[i]*(c[k]-c[j]) \]

\[\frac{f[j]-f[k]}{c[k]-c[j]}<\frac{b[i]}{a[i]} \]

我们把关于j,k的放在不等式左边,然后把常数(已知的)都放右边
这样左边的式子就是一个斜率一样的东西
即,j点的坐标为(\(f[j]\),\(-c[j]\))
然后我们可以把整个式子化成一个一次函数的形式:

\[f[i]=a[i]*f[j]+b[i]*c[j]+d[i] \]

其中我们知道了j的坐标,所以:

\[a[i]*f[j]=b[i]*(-c[j])+f[i]-d[i] \]

\[f[j]=\frac{b[i]}{a[i]}*(-c[j])+\frac{f[i]-d[i]}{a[i]} \]

所以我们发现了这是一个\(y=kx+b\)的形式
可以看成一条一次函数。
找决策点的任务是使得我们得出来的\(f[i]\)最小,所以,应使得上述解析式在j为一定值时的截距最小,只是我们可以想到维护一个下凸壳。

为什么要维护凸壳?

我们可以把每次查询的过程理解为一个一次函数,已经确定了斜率,现在我们在坐标系中有一堆散点,然后我们要把这个直线从下往上推上去,直到经过第一个决策点j为止,则在这时,直线的截距必定最小,这个j就是我们所说的最优决策点。
我们来看一下图:
在这里插入图片描述
如上图,我们在一堆散点中维护出一个下凸壳,在每次查询i时会有一个斜率,则这个斜率就可以在我们维护的下凸壳中找到一个最优决策点j,使得截距最小。

如何维护下凸壳?

维护一个下凸壳,我们可以使用一个队列(其实严格意义不能说是一个队列,因为它队首可以出,队尾可以进可以出)。
上面看起来很懵逼,我们来看一下每次要什么操作:

 int h=1,t;
 d[t=1]=0;
 for (i=1;i<=n;++i){
    while(h<t&&本次询问i的斜率>队首和队首后一个构成的直线的斜率) ++h;
    f[i]=a[i]*f[d[h]]+b[i]*c[j]+d[i];
    while(t>h&&即将放入队列中的i点和队尾的斜率<=队尾和其前一个点构成的斜率) --t;
    d[++t]=i;
 }

其中,第二个while的斜率比较我用了<=,我也不知道为什么,在做HDU3507的时候,交到oj上时,不加这个=就错了...

上面代码仅限维护下凸壳,如果是上凸壳的话,注意一下不等号的方向。

斜率不单调?

有两种奇怪的情况:

1.询问点i所对应的斜率不单调

这种情况比较普通,我们就不能像上面那样,直接比较队首了,因为...显然啊。
那么我们可以直接在这个单调的凸壳上面二分,这样也可以保证有一个n log的时间。

2.决策点j的横坐标不单调

这个的话,因为作者能力有限,并不会,只知道可以用平衡树或者线段树之类的维护。
有兴趣的可以了解一下李超树

注意

比较斜率的时候可能会有严重的精度问题,所以最好把等式两边斜率的分子分母交叉相乘来判断。更加重要的是,因为不等式两边同时乘或除以一个负数,不等号的方向会改变,所以我们要保证分子分母都是非负数再乘过去,不然会出很大的问题。

例题

上面提到的HDU3507就是道好题,大家有兴趣可以去看看。

#include<cstdio>
#include<iostream>
#include<cstring>
using namespace std;

const int maxn=5e5+10;

int n,m;
int c[maxn];
int f[maxn];
int sum[maxn];
int d[maxn*4];

int X(int x){
    return sum[x];
}

int Y(int x){
    return f[x]+sum[x]*sum[x];
}

int main(){
    int i,j; 
    while(scanf("%d%d",&n,&m)!=EOF){
        for (i=1;i<=n;++i)
            scanf("%d",&c[i]),sum[i]=sum[i-1]+c[i];
        
        memset(f,127,sizeof(f)),f[0]=0;
        int h=1,t;
        d[t=1]=0;
        for (i=1;i<=n;++i){
            while(h<t&&2*sum[i]*(X(d[h+1])-X(d[h]))>Y(d[h+1])-Y(d[h])) ++h;
            f[i]=f[d[h]]+(sum[i]-sum[d[h]])*(sum[i]-sum[d[h]])+m;
            while(t>h&&(Y(d[t])-Y(i))*(X(d[t-1])-X(d[t]))<=(X(d[t])-X(i))*(Y(d[t-1])-Y(d[t]))) --t;
            d[++t]=i;
        }
        
        printf("%d\n",f[n]);
    }
}

总结

我们来总结一下做这种题的一般步骤,加深理解:
1.首先列出dp方程。
2.然后像上面第一点一样先用j和k之间的决策优秀性来解出不等式,确定每个决策点的横纵坐标。
3.按照刚刚确定的横纵坐标来把原方程移项,拆成y=kx+b的形式(小技巧:观察,有关于i的常量和关于j的变量相乘的项,一般关于i的常量就是斜率)。
4.可以开始确定维护凸壳的形状,以及维护的方式和查询的方式。
5.切掉这道题。
6.AK这套比赛!

本人太菜,如有问题还请大佬多多海涵!

posted @ 2019-08-10 19:42  Chandery  阅读(162)  评论(0编辑  收藏  举报