zoukankan      html  css  js  c++  java
  • 关于动态规划的斜率优化

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

    使用条件

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

    \[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这套比赛!

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

  • 相关阅读:
    今天VSS 了一把
    中文字母检索
    当心! 您也可能犯得js错 eval()不等于eval("")!
    腾讯微博邀请码2010年6月9日11:14:28
    存储过程原理
    腾讯微博邀请码2010年5月25日16:44:24
    《QQ我的好友想到的信息架构》
    8小时之外(Beyond the 8 Hours)
    超搞笑漫画比喻!如果浏览器是出行工具
    Nginx环境下配置PHP使用的SSL认证(https)
  • 原文地址:https://www.cnblogs.com/Chandery/p/11332722.html
Copyright © 2011-2022 走看看