[算法]斜率優化


【引入】

有些$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$

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM