斜率最佳化 DP

FlyPancake發表於2024-10-05

斜率最佳化DP

在單調佇列最佳化過程中,轉移方程被拆成了兩部分,第一部分僅與 \(j\) 有關,而另一部分僅與 \(i\) 有關,因此我們可以利用單調佇列僅維護與 \(j\) 有關的部分,實現問題的快速求解。

但仍然有很多轉移方程,\(i\)\(j\) 是緊密相關的,這個時候單調佇列最佳化就不適用了,例如如下轉移方程格式:

\[y[j] = x[j] \times k[i]+dp[i]+base[i] \]

其中 \(x[j]\)\(y[j]\) 是一個僅與 \(j\) 以及 \(dp[j]\) 有關的式子,不受 \(i\) 影響;而 \(k[i]\)\(base[i]\) 則是一個僅與 \(i\) 有關的式子,不受 \(j\) 影響。因此這個式子的唯一未知量就是 \(dp[i]\),如何選擇合適的 \(j\) 來求取最優的 \(dp[i]\) 就是該轉移方程的關鍵問題。

仔細觀察上述式子,不難發現其實這是一個形如 \(y=kx+b\) 的式子,\(k\)\(i\) 決定,而 \(x\)\(y\) 則由 \(j\) 決定,因此我們可以將這個問題抽象為,平面上有很多個點 \((x[j] , y[j])\),然後我們用斜率為 \(k[i]\) 的直線去靠近這些點,希望找一個使 \(b\) 最大的 \(j\)

而這種情況下,我們就需要維護上/下凸殼,且需要根據具體的題意,如 \(k[i]\) 是否遞增,\(k[i]\) 是否始終為正,\(k[i]\) 是否有可能為負等問題來選擇具體的維護和轉移方法,可能會涉及 set 以及二分的使用。
總結一下,在斜率最佳化問題中,每一個 \(j\) 都是一個二維平面上的點 \((x[j], y[j])\),轉移時我們需要用斜率為 \(k[i]\) 的直線來靠近這些點,使得 \(b[i]\) 達到最優。

P3195 [HNOI2008] 玩具裝箱

題目大意:

共有 \(n\) 個玩具,第 \(i\) 個玩具長度為 \(c_i\),如果將第 \(i\) 個玩具到第 \(j\) 個玩具放到一個容器中,則該容器長度 \(x=j-i+\sum_{k=i}^jc_k\)。製作一個容器的費用為 \((x-L)^2\),其中 \(L\) 為常數,可以製作若干個容器。求將所有玩具都放入容器中的費用最小值。

\(n\leq 5\times 10^4,1\leq L,c_i\leq10^7\)


考慮樸素 DP:

\(dp[i]\) 表示第 \(i\) 個玩具被裝起來後的總費用最小值,\(sum[i]\) 表示前 \(i\) 個玩具長度之和。可得轉移方程:

\[dp[i] = \min\{dp[j]+(sum[i]-sum[j]+(i-j-1)-L)^2\} \quad (j < i) \]

\(i\) 製作完從 \(j\) 製作完轉移而來,新容器裝的玩具實際是 \([j+1, i]\)

時間複雜度為 \(O(n^2)\),運氣好可能能過。

考慮最佳化:

將後面改寫成一段只與 \(i\) 有關,一段只與 \(j\) 有關。令 \(a[i] = sum[i]+i, b[i] = sum[j]+j+L+1\)

則(先省略 \(\min\)

\[dp[i] = dp[j]+(a[i]-b[j])^2 \]

平方難以最佳化,展開得

\[dp[i] = dp[j]+a[i]^2-2\cdot a[i]\cdot b[i]+b[j]^2 \]

有點斜率最佳化的影子了,移項得

\[dp[j]+b[j]^2 = 2\cdot a[i]\cdot b[j] + dp[i] - a[i]^2 \]

\(dp[j]+b[j]^2\) 看作 \(y[j]\)\(b[j]\) 看作 \(x[j]\)\(2\cdot a[i]\) 看作 \(k[i]\),對於每個 \(i\) 來說,\(-a[i]^2\)\(2\cdot a[i]\) 都是確定的,式子可寫作

\[y[j] = k[i]\times x[j]+dp[i]+base[i] \]

接下來就是求解。

\(dp[i]\) 的含義轉化為當上述直線過點 \(P(b[j], dp[j]+b[j]^2)\) 時,直線在 \(y\) 軸的截距加上 \(a[i]^2\)(對 \(i\) 來說是一個定值)的最小值。所以找到可能直線的截距的最小值就行了。

因此,類似線性規劃,我們將這條直線從下往上平移,直到過第一個符合要求的點時停下,此時截距即為最小。

畫出影像如下(紅色為目標直線):

slope1.png

結合影像分析可知,本題中可能為最優的 \(P\) 點(圖中用直線連線)組成了一個下凸包。

顯然,凸包中相鄰兩點斜率是單調遞增的。

而目標直線的斜率 \(2 \cdot a[i]\)\(i\) 也是單調遞增的。

\(A, B\) 兩點之間斜率為 \(slope(A, B)\)。由影像易知,滿足條件的第一個 \(P_j\) 即為第一個 \(slope(P_j, P_{j+1}) > 2\cdot a[i]\) 的點。

因為凸包和直線斜率均遞增,我們可以用單調佇列來維護這個凸包:

設隊首為 \(head\),隊尾為 \(tail\)。需要讓隊首元素為最優的 \(P_j\) 的下標 \(j\)

  1. 對隊首:

    隊首元素一直右移直到滿足 \(slope(P_{head},P_{head+1})\ge 2\cdot a[i]\)

    解釋:如果 \(slope(P_{head},P_{head+1}) < 2\cdot a[i]\),顯然 \(P_j\) ​不是最優。可直接刪去,因為目標直線斜率單調遞增,所以當前刪去的 \(P_j\) 一定對之後的 \(dp[i]\) 也不是最優,不會造成影響。

  2. 此時隊首的點即為最優,根據它計算得出 \(dp[i]\)

  3. 對隊尾:

    隊尾元素一直左移直到滿足 \(slope(P_{tail-1},P_{tail})\le slope(P_{tail-1},P_i)\)

    解釋:如果 \(slope(P_{tail-1},P_{tail}) > slope(P_{tail-1},P_i)\),說明 \(P_{tail}\) 在凸包內部,一定沒有 \(P_i\) 優,因此可以刪去。

    slope2.png

  4. 在隊尾插入 \(P_i\)

最後注意初始化時要加入單調佇列的點為 \(P_0\) 而不是 \(P_1\),否則就變成了第一個物品必須單獨裝。


樸素字首和最佳化 DP 程式碼:

Code
#include<bits/stdc++.h>
using namespace std;
#define DEBUG(a) cout<<"Dline[ "<<__LINE__<<" ]: "<<(a)<<"\n";
#define ll long long
const int N = 5e4+5;

ll c[N], sum[N];
ll dp[N]; // dp[i] 表示裝完第 i 個玩具後所需費用的最小值

int main(){
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    int n, L; cin>>n>>L;
    for(int i=1; i<=n; i++){
        cin>>c[i];
        sum[i] = sum[i-1]+c[i];
    }
    for(int i=1; i<=n; i++)
        dp[i] = INT64_MAX;
    for(int i=1; i<=n; i++){
        for(int j=0; j<i; j++){
            dp[i] = min(dp[i], dp[j]+(sum[i]+i-sum[j]-j-1-L)*(sum[i]+i-sum[j]-j-1-L)); // [j+1, i]
        }
    }
    cout<<dp[n];
    return 0;
}

正解加上斜率最佳化 DP 程式碼:

#include<bits/stdc++.h>
using namespace std;
#define DEBUG(a) cout<<"Dline[ "<<__LINE__<<" ]: "<<(a)<<"\n";
#define ll long long
#define db double
const int N = 5e4+5;

int n, L;
ll c[N], sum[N];
ll Q[N], dp[N]; // dp[i] 表示裝完第 i 個玩具後所需費用的最小值

db a(int x){return sum[x]+x;}
db b(int x){return a(x)+L+1;}
db X(int x){return b(x);}
db Y(int x){return (db)dp[x]+b(x)*b(x);}
db slope(int a, int b){return (Y(a)-Y(b))/(X(a)-X(b));}

int main(){
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    cin>>n>>L;
    for(int i=1; i<=n; i++){
        cin>>c[i];
        sum[i] = sum[i-1]+c[i];
    }
    int head = 1, tail = 1;
    for(int i=1; i<=n; i++){
        while(head<tail && slope(Q[head], Q[head+1])<2*a(i)) head++;
        dp[i] = dp[Q[head]]+(a(i)-b(Q[head]))*(a(i)-b(Q[head]));
        while(head<tail && slope(i, Q[tail-1])<slope(Q[tail], Q[tail-1])) tail--;
        Q[++tail] = i;
    }
    cout<<dp[n];
    return 0;
}

相關文章