1. 程式人生 > 其它 >斜率優化動態規劃 學習筆記

斜率優化動態規劃 學習筆記

首先看這樣一個問題:

洛谷 P3195 [HNOI2008]玩具裝箱
題目大意:
\(n\) 個物品排成一行,第 \(i\) 個物品權值為 \(C_i\) ,現要求將這些物品分成若干段,每段的花費為 \(((\sum_{i=l}^{r}{C_i})-L)^2\) (其中 \(l\),\(r\) 為這一段的左右端點, \(L\) 為給定常數),問最小的總花費.保證 \(1 \leq n \leq 5 \times 10^4\)\(1 \leq L \leq 10^7\)\(1 \leq C_i \leq 10^7\) .

這題顯然是一道 DP 題. 令 \(dp_{i}\) 為考慮前 \(i\)

個物品的最小代價, \(sum_i=\sum_{j=1}^{i}C_j\) , 可以想出這樣的狀態轉移方程:

\[dp_i=\min_{0\le j<i}\{dp_j+(sum_i-sum_{j}-L)^2\} \]

然而, 如果暴力轉移時間複雜度是 \(\Theta(n^2)\) 的, 顯然會T, 有什麼辦法優化嗎?

動態規劃優化的一個重要思路是把可能決策的範圍縮小 (即排除不可能的決策) . 順著這個思路試試看?

首先我們觀察狀態轉移方程, 發現這個 \(\min\) 函式很礙事, 把它去掉:

\[dp_i=dp_j+(sum_i-sum_{j}-L)^2 \]

這個方程太長了, 所以考慮令 \(a_i=sum_i-L\)

, \(b_i=sum_i\) , 代入得:

\[dp_i=dp_j+(a_i-b_j)^2 \]

這個平方也很礙事, 把它展開:

\[dp_i=dp_j+{a_i}^2-2a_ib_j+{b_j}^2 \]

把跟 \(i\) 有關的移到等號一邊, 跟 \(j\) (讀作"勾") 有關的放到另一邊:

\[2a_ib_j+dp_i-{a_i}^2=dp_j+{b_j}^2 \]

......感覺直到現在我們做的都是一些很常規的操作...... 但是如果我們放一個直線的表示式跟它對比一下?

\((2a_i)b_j+(dp_i-{a_i}^2)=(dp_j+{b_j}^2)\)
\(kx+b=y\)

於是我們驚奇地發現, 狀態轉移方程竟然可以看作一條斜率為 \(2a_i\) , 截距為 \(dp_i-{a_i}^2\) 的直線和一個在 \((b_j,dp_j+{b_j}^2)\) 的點(我管它叫決策點)!

所以, "轉移" 這個過程就可以理解為找到一個斜率為 \(2a_i\) 的直線交於一個已有的決策點.

其中, 紅色直線表示 \(dp_i\) 對應的直線,綠色直線表示一個可能的 \(dp_j\) 對應的直線, 點 A 表示這個 \(dp_j\) 對應的決策點, 點 B 表示 \(dp_i\) 對應的決策點.

所以我們怎麼找一條最優的直線, 使得 \(dp_i\) 最小? 難道要列舉 \(j\) 嗎?

當然不用. 因為 \(dp_i-{a_i}^2\) 只與 \(i\) 有關, 所以只要這條直線的截距最小, \(dp_i\) 就是最小的. 我們假設平面上已經有一堆可供轉移的決策點和直線:

可以想象 \(dp_i\) 對應的直線從下往上移動 (別忘了這條直線的斜率不變) , 顯然它第一個接觸到的點就是最優決策點.

如果您學過計算幾何 (我沒學過QAQ) , 您可以馬上發現潛在的最優決策點都在這一堆點的下凸包上!

所以我們只需要用一個單調佇列來維護這個凸包就可以了.

具體怎麼維護等到以後再寫罷, 博主累了.

綜上所述, 我們通過發掘狀態轉移方程的性質做到了攤還 \(\Theta(1)\) 的轉移, 非常優秀.

程式碼:

#include <iostream>
#include <queue>
using namespace std;
typedef long long ll;
#define int ll
const int MAXN=50000;
int n,L,c[MAXN+5],sum[MAXN+5],dp[MAXN+5];
deque<int> q;
int a(int i){return sum[i]+i;}
int b(int i){return sum[i]+i+L+1;}
int X(int i){return b(i);}
int Y(int i){return (dp[i])+(b(i)*b(i));}
double slope(int i,int j){return double(Y(i)-Y(j))/double(X(i)-X(j));}
signed main(){
    ios::sync_with_stdio(false);
    cin>>n>>L;
    for(int i=1;i<=n;i++){
        cin>>c[i];sum[i]=sum[i-1]+c[i];
    }
    q.push_back(0);
    for(int i=1;i<=n;i++){
        double qwq=114514.1919810;
        if(q.size()>=2)qwq=slope(q[0],q[1]);
        while(q.size()>=2&&slope(q[0],q[1])<double(2*a(i))){
            q.pop_front();
        }
        int j=q.front();
        dp[i]=dp[j]+(a(i)-b(j))*(a(i)-b(j));
        while(q.size()>=2&&slope(q[q.size()-1],q[q.size()-2])>slope(q[q.size()-2],i))q.pop_back();
        q.push_back(i);
    }
    cout<<dp[n]<<endl;
    return 0;
}