斜率最佳化DP
在單調佇列最佳化過程中,轉移方程被拆成了兩部分,第一部分僅與 \(j\) 有關,而另一部分僅與 \(i\) 有關,因此我們可以利用單調佇列僅維護與 \(j\) 有關的部分,實現問題的快速求解。
但仍然有很多轉移方程,\(i\) 和 \(j\) 是緊密相關的,這個時候單調佇列最佳化就不適用了,例如如下轉移方程格式:
其中 \(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\) 個玩具長度之和。可得轉移方程:
\(i\) 製作完從 \(j\) 製作完轉移而來,新容器裝的玩具實際是 \([j+1, i]\)。
時間複雜度為 \(O(n^2)\),運氣好可能能過。
考慮最佳化:
將後面改寫成一段只與 \(i\) 有關,一段只與 \(j\) 有關。令 \(a[i] = sum[i]+i, b[i] = sum[j]+j+L+1\)。
則(先省略 \(\min\))
平方難以最佳化,展開得
有點斜率最佳化的影子了,移項得
將 \(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]\) 都是確定的,式子可寫作
接下來就是求解。
\(dp[i]\) 的含義轉化為當上述直線過點 \(P(b[j], dp[j]+b[j]^2)\) 時,直線在 \(y\) 軸的截距加上 \(a[i]^2\)(對 \(i\) 來說是一個定值)的最小值。所以找到可能直線的截距的最小值就行了。
因此,類似線性規劃,我們將這條直線從下往上平移,直到過第一個符合要求的點時停下,此時截距即為最小。
畫出影像如下(紅色為目標直線):
結合影像分析可知,本題中可能為最優的 \(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\)。
-
對隊首:
隊首元素一直右移直到滿足 \(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]\) 也不是最優,不會造成影響。
-
此時隊首的點即為最優,根據它計算得出 \(dp[i]\)。
-
對隊尾:
隊尾元素一直左移直到滿足 \(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\) 優,因此可以刪去。
-
在隊尾插入 \(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;
}