[算法]斜率优化

【引入】

有些$DP$方程可以转化成$f[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]$的项。它不能分解为只与$i$或$j$有关的部分。若用单调队列优化方法就不好使了。
这里学习一种新的优化方法,叫做斜率优化


【例题】HDU 3507  print artical

【题目大意】

输出$N$个数字$a[N]$,输出的时候可以连续的输出,每连续输出一串,它的费用是 “这串数字和的平方加上一个常数$M$”。其中$N≤500000$。

【例题分析】

对于这样一个题目,我们先规定以下变量:

  • $dp[i]$:输出到$i$的时候最少的花费
  • $sum[i]$:从$a[1]$到$a[i]$的数字和。

于是方程就是:$$dp[i]=dp[j]+M+(sum[i]-sum[j])^2$$

很显然这个$DP$式时间复杂度为$O(N^2)$。题目的数字有$500000$个,很明显,若不对这个$DP$式加以修饰,是一定会超时的。

那么该怎么办呢?这下就需要我们的斜率优化对于$O(N^2)$的时间复杂度降维

首先,我们对这个式子化简:

我们考虑两个决策点$k$与$j$,如果决策$j$更优,那么也就是

$$dp[j]+M+(sum[i]-sum[j])^2<dp[k]+M+(sum[i]-sum[k])^2$$

消去共同项,得:

$$dp[j]+sum[j]^2-2×sum[i]×sum[j]<dp[k]+sum[k]^2-2×sum[i]×sum[k]$$

即$$dp[j]+sum[j]^2-(dp[k]+sum[k]^2)<2×sum[i]×(sum[j]-sum[k])$$

若$j>k$,则$$sum[j]-sum[k]>0$$

可得$${\frac{{dp\text{[}j\text{]}+sum\text{[}j\mathop{{\text{]}}}\nolimits^{{2}}-\text{(}dp\text{[}k\text{]}+sum\text{[}k\mathop{{\text{]}}}\nolimits^{{2}}\text{)}}}{{2 \times \text{(}sum\text{[}j\text{]}-sum\text{[}k\text{]}\text{)}}}}<sum[i]$$

反之,若$j<k$,则$$sum[j]-sum[k]<0$$

则可得$$
{\frac{{dp\text{[}j\text{]}+sum\text{[}j\mathop{{\text{]}}}\nolimits^{{2}}-\text{(}dp\text{[}k\text{]}+sum\text{[}k\mathop{{\text{]}}}\nolimits^{{2}}\text{)}}}{{\left. 2*\text{(}sum\text{[}j\text{]}-sum\text{[}k\text{]} \right) }}}>sum[i]$$

我们把$dp[j]+sum[j]^2$看做是$y_j$,把$2×sum[j]$看成是$x_j$。

左边$\frac{y_j-y_k}{x_j-x_k}$似乎是斜率的表示?

若$j>k$,则$\frac{y_j-y_k}{x_j-x_k}<sum[i]$等价于决策$j$优于$k$。

若$j<k$,则$\frac{y_j-y_k}{x_j-x_k}>sum[i]$等价于决策$j$优于$k$。
感性理解就是:如果两个决策点的斜率小于$sum[i]$,则靠后的决策点更优;否则靠前的决策点更优。


 

更强的性质:

若有三个决策点,满足$k<j<i$且$g[i,j]<g[j,k]$,则$j$点永远不可能成为最优决策点,可以直接将它从决策点集合中去掉。

但是这是为什么呢?

分三种情况讨论:

设当前点为$a$

  • 如果$g[i,j]$与$g[j,k]$均小于$sum[a]$,则$i$比$j$优,$j$比$k$。
  • 如果$g[i,j]$与$g[j,k]$均大于$sum[a]$,则$k$比$j$优,$j$比$i$优。
  • 如果$g[i,j]<sum[a]$且$g[i,j]>sum[a]$,则$i$比$j$优,$k$比$j$优。

不论如何,$j$都无法成为最佳决策点,所以可以排除$j$。

于是,所有的决策点满足一个下凸包性质。

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

设$k<j<i$。

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

这样,从左到右,斜率之间就是单调递增的了。当我们的最优解取得在$j$点的时候,那么$k$点不可能再取得比$j$点更优的解了,于是$k$点也可以排除。

换句话说,$j$点之前的点全部不可能再比$j$点更优了,可全部从解集中排除。这是为什么呢?因为$sum[i]$随着$i$的增长也是单调递增的。

所以,对于两个决策点$j$和$k$,设$j<k$。如果$k$优于$j$,则以后$k$永远优于$j$,则$j$及之前的决策点都可以删除了。

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

  • 用一个单调队列来维护解集。
  • 假设队列中从头到尾已经有元素$a,b,c$。那么当$d$要入队的时候,我们维护队列的下凸性质,即如果$g[d,c]<g[c,b]$,那么就将$c$点删除。直到找到$g[d,x]≥g[x,y]$为止,并将$d$点加入在该位置中。
  • 找最佳决策点时,设当前求解状态为$i$,从队头开始,如果已有元素$a,b,c$,当i点要求解时,如果$g[b,a]<sum[i]$,那么说明$b$点比$a$点更优,$a$点可以排除,于是$a$出队,直到第一次遇到$g[j,j-1]>sum[i]$,此时$j-1$即为最佳决策点。

设有三个点$i,j,k$。其中$x_i>x_j>x_k$。如何判断三点是上凸还是下凸?

用向量的叉积运算即可。

向量可以看做是二维平面坐标中的有向线段。向量的起点可以自由选择,即可以把它在平面内任意平移。平移过后的向量与原平移前完全等价。

如果一条有向线段的起点为$(x_1,y_1)$,终点为$(x_2,y_2)$。我们可以将它平移,使得起点位置为$(0,0)$,终点位置为$(x_2-x_1,y_2-y_1)$。

此时向量的大小和方向不变。我们以后谈及向量,都默认它的起点在$(0,0)$处,而只以它的终点表示该向量。

设有向量$p_1(x_1,y_1)$,$p_2(x_2,y_2)$。他们的叉乘为$p_1×p_2=(x_1*y_2-x_2*y_1)$。

叉乘的物理意义为以向量$p_1$和$p_2$为相邻两边的平行四边形的有向面积。

左图为正向有向面积,右图为负向有向面积。

 

 

 

 

 

 

 

平面四边形在两向量的顺时针方向,则为正,反之则为负。如何判断$p_1$与$p_2$的位置关系?

  • 若$p_1×p_2>0$,则$p_2$在$p_1$的逆时针方向;
  • 若$p_1×p_2<0$,则$p_2$在$p_1$的顺时针方向;
  • 若$p_1×p_2=0$,则$p_1$与$p_2$方向重合。

所以我们可以令向量$p_1=(x_j-x_k,y_j-y_k),p_2=(x_i-x_j,y_j-y_k)$,再用叉积即可判断$i,j,k$是上凸还是下凸。
另外注意:比较斜率避免用除法。
下见代码

#include<iostream>
#include<string>
using namespace std;
int dp[500005];
int q[500005];
int sum[500005];
int head,tail,n,m;
int getDP(int i,int j)
{
    return dp[j]+m+(sum[i]-sum[j])*(sum[i]-sum[j]);
}
int getUP(int j,int k)  //yj-yk的部分
{
    return dp[j]+sum[j]*sum[j]-(dp[k]+sum[k]*sum[k]);
}

int getDOWN(int j,int k) //xj-xk的部分
{
    return 2*(sum[j]-sum[k]);
}
int main()
{
    int i;
    while(scanf("%d%d",&n,&m)==2)
    {
        for(i=1;i<=n;i++)
            scanf("%d",&sum[i]);
        sum[0]=dp[0]=0;
        for(i=1;i<=n;i++)
            sum[i]+=sum[i-1];
        head=tail=0;
        q[tail++]=0;
        for(i=1;i<=n;i++)
        {
            while(head+1<tail && getUP(q[head+1],q[head])<=sum[i]*getDOWN(q[head+1],q[head]))
                head++;
            dp[i]=getDP(i,q[head]);
            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]))
                tail--;
            q[tail++]=i;
        }
        printf("%d\n",dp[n]);
    }
    return 0;
}

练习题:

$HDU2829,HDU3480,POJ3709$

 

posted @ 2019-10-05 16:21  南枙向暖  阅读(938)  评论(0编辑  收藏  举报