zoukankan      html  css  js  c++  java
  • 斜率优化DP

    我们知道,有些DP方程可以转化成DP[i]=f[j]+x[i]的形式,其中f[j]中保存了只与j相关的量。这样的DP方程我们可以用单调队列进行优化,从而使得O(n^2)的复杂度降到O(n)。

    可是并不是所有的方程都可以转化成上面的形式,举个例子:dp[i]=dp[j]+(x[i]-x[j])*(x[i]-x[j])。如果把右边的乘法化开的话,会得到x[i]*x[j]的项。这就没办法使得f[j]里只存在于j相关的量了。于是上面的单调队列优化方法就不好使了。

    这里学习一种新的优化方法,叫做斜率优化,其实和凸包差不多,下面会解释。

    举例子说明是最好的!HDU 3507,很适合的一个入门题。http://acm.hdu.edu.cn/showproblem.php?pid=3507

    大概题意就是要输出N个数字a[N],输出的时候可以连续连续的输出,每连续输出一串,它的费用是 “这串数字和的平方加上一个常数M”。

    我们设dp[i]表示输出到i的时候最少的花费,sum[i]表示从a[1]到a[i]的数字和。于是方程就是:

    dp[i]=dp[j]+M+(sum[i]-sum[j])^2;

    很显然这个是一个二维的。题目的数字有500000个,不用试了,二维铁定超时了。那我们就来试试斜率优化吧,看看是如何做到从O(n^2)复杂度降到O(n)的。

    分析:

    我们假设k<j<i。如果在j的时候决策要比在k的时候决策好,那么也是就是dp[j]+M+(sum[i]-sum[j])^2<dp[k]+M+(sum[i]-sum[k])^2。(因为是最小花费嘛,所以优就是小于)

    两边移项一下,得到:(dp[j]+num[j]^2-(dp[k]+num[k]^2))/(2*(num[j]-num[k]))<sum[i]。我们把dp[j]-num[j]^2看做是yj,把2*num[j]看成是xj。

    那么不就是yj-yk/xj-xk<sum[i]么?   左边是不是斜率的表示? 

    那么yj-yk/xj-xk<sum[i]说明了什么呢?  我们前面是不是假设j的决策比k的决策要好才得到这个表示的? 如果是的话,那么就说明g[j,k]=yj-jk/xj-xk<sum[i]代表这j的决策比k的决策要更优。

    关键的来了:现在从左到右,还是设k<j<i,如果g[i,j]<g[j,k],那么j点便永远不可能成为最优解,可以直接将它踢出我们的最优解集。为什么呢?

    我们假设g[i,j]<sum[i],那么就是说i点要比j点优,排除j点。

    如果g[i,j]>=sum[i],那么j点此时是比i点要更优,但是同时g[j,k]>g[i,j]>sum[i]。这说明还有k点会比j点更优,同样排除j点。

    排除多余的点,这便是一种优化!

    接下来看看如何找最优解。

    设k<j<i。

    由于我们排除了g[i,j]<g[j,k]的情况,所以整个有效点集呈现一种上凸性质,即k j的斜率要大于j i的斜率。

    这样,从左到右,斜率之间就是单调递减的了。当我们的最优解取得在j点的时候,那么k点不可能再取得比j点更优的解了,于是k点也可以排除。换句话说,j点之前的点全部不可能再比j点更优了,可以全部从解集中排除。

    于是对于这题我们对于斜率优化做法可以总结如下:

    1,用一个单调队列来维护解集。

    2,假设队列中从头到尾已经有元素a b c。那么当d要入队的时候,我们维护队列的上凸性质,即如果g[d,c]<g[c,b],那么就将c点删除。直到找到g[d,x]>=g[x,y]为止,并将d点加入在该位置中。

    3,求解时候,从队头开始,如果已有元素a b c,当i点要求解时,如果g[b,a]<sum[i],那么说明b点比a点更优,a点可以排除,于是a出队。最后dp[i]=getDp(q[head])。

    View Code
     1 #include<iostream>
     2 #include<string>
     3 using namespace std;
     4 
     5 int dp[500005];
     6 int q[500005];
     7 int sum[500005];
     8 int head,tail,n,m;
     9 
    10 int getDP(int i,int j)
    11 {
    12     return dp[j]+m+(sum[i]-sum[j])*(sum[i]-sum[j]);
    13 }
    14 
    15 int getUP(int j,int k)  //yj-yk的部分
    16 {
    17     return dp[j]+sum[j]*sum[j]-(dp[k]+sum[k]*sum[k]);
    18 }
    19 
    20 int getDOWN(int j,int k) //xj-xk的部分
    21 {
    22     return 2*(sum[j]-sum[k]);
    23 }
    24 
    25 int main()
    26 {
    27     int i;
    28     freopen("D:\\in.txt","r",stdin);
    29     while(scanf("%d%d",&n,&m)==2)
    30     {
    31         for(i=1;i<=n;i++)
    32             scanf("%d",&sum[i]);
    33         sum[0]=dp[0]=0;
    34         for(i=1;i<=n;i++)
    35             sum[i]+=sum[i-1];
    36         head=tail=0;
    37         q[tail++]=0;
    38         for(i=1;i<=n;i++)
    39         {
    40             while(head+1<tail && getUP(q[head+1],q[head])<=sum[i]*getDOWN(q[head+1],q[head]))
    41                 head++;
    42             dp[i]=getDP(i,q[head]);
    43             while(head+1<tail && getUP(i,q[tail-1])*getDOWN(q[tail-1],q[tail-2])<=getUP(q[tail-1],q[tail-2])*getDOWN(i,q[tail-1]))
    44                 tail--;
    45             q[tail++]=i;
    46         }
    47         printf("%d\n",dp[n]);
    48     }
    49     return 0;
    50 }
  • 相关阅读:
    mysql 内联接、左联接、右联接、完全联接、交叉联接 区别
    JS 时间字符串与时间戳之间的转换
    MySQL性能优化的最佳20条经验
    ++i 与 i++ 的区别
    === 与 == 区别
    SC命令创建和删除windows服务
    杂记
    linux 文件编程
    u-boot 启动过程
    简单冒泡法
  • 原文地址:https://www.cnblogs.com/ka200812/p/2621345.html
Copyright © 2011-2022 走看看