背景
基于例題《任務安排》逐步推導進行斜率優化。
引入
例題:P2365 任務安排
考慮動態規劃。使用 d p i , j dp_{i,j} dpi,j? 表示前 i i i 個任務分了 j j j 段的最小費用。
顯然,有 d p i , j = min ? k = 1 i ? 1 ( d p i , j , d p k , j ? 1 + ( t o t i ? t o t k ) ) ? ( s u m [ i ] + s ? j ) ) dp_{i,j} = \min_{k=1}^{i-1} (dp_{i,j},dp_{k,j-1} + (tot_i-tot_k))*(sum[i]+s*j)) dpi,j?=mink=1i?1?(dpi,j?,dpk,j?1?+(toti??totk?))?(sum[i]+s?j))? 。
- s u m i sum_i sumi? 表示 c i c_i ci? 的前綴和。
- t o t i tot_i toti? 表示 t i t_i ti? 的前綴和。
前綴和優化后,時間復雜度 O ( n 3 ) O(n^3) O(n3),得到 60pts.
代碼
#include <bits/stdc++.h>
using namespace std;
int n,s,ans,t[5005],c[5005],dp[5005][5005],sum[5005],tot[5005];
int main()
{cin >> n >> s;for (int i=1;i<=n;i++){cin >> t[i] >> c[i];sum[i] = sum[i-1] + t[i];tot[i] = tot[i-1] + c[i];}memset(dp,0x3f,sizeof(dp));ans = 0x3f3f3f3f;dp[0][0] = 0;for (int i=1;i<=n;i++){for (int j=1;j<=i;j++){for (int k=0;k<i;k++){dp[i][j] = min(dp[i][j],dp[k][j-1] + (tot[i]-tot[k])*(sum[i]+s*j));} } }for (int i=1;i<=n;i++){ans = min(ans,dp[n][i]);}cout<<ans;return 0;
}
如何進一步優化呢?
我們發現,可以把有關 s s s 的計算在前面完成。也就是 費用提前計算 ,就不需要枚舉分的段數了。
得到狀態轉移方程 d p i = min ? ( d p i , d p j + s u m i ? t o t i ? s u m i ? t o t j + t o t n ? s ? t o t j ? s ) dp_i = \min(dp_i,dp_j + sum_i*tot_i-sum_i*tot_j + tot_n*s-tot_j*s) dpi?=min(dpi?,dpj?+sumi??toti??sumi??totj?+totn??s?totj??s)
代碼
#include <bits/stdc++.h>
using namespace std;
long long n,s,ans,t[5005],c[5005],dp[5005],sum[5005],tot[5005];
int main()
{cin >> n >> s;for (int i=1;i<=n;i++){cin >> t[i] >> c[i];sum[i] = sum[i-1] + t[i];tot[i] = tot[i-1] + c[i];dp[i] = 1e18;}ans = 1e18;dp[0] = 0;for (int i=1;i<=n;i++){for (int j=0;j<i;j++){dp[i] = min(dp[i],dp[j] + sum[i]*(tot[i]-tot[j]) + (tot[n]-tot[j])*s);} }cout<<dp[n];return 0;
}
正文
狀態轉移方程 d p i = min ? ( d p i , d p j + s u m i ? t o t i ? s u m i ? t o t j + t o t n ? s ? t o t j ? s ) dp_i = \min(dp_i,dp_j + sum_i*tot_i-sum_i*tot_j + tot_n*s-tot_j*s) dpi?=min(dpi?,dpj?+sumi??toti??sumi??totj?+totn??s?totj??s)
把與 i , j i,j i,j 有關的各單獨放在一起,得到 d p i = min ? ( d p i , d p j + s u m i ? t o t i + t o t n ? s ? t o t j ? ( s u m i + s ) ) dp_i = \min(dp_i,dp_j + sum_i*tot_i + tot_n*s - tot_j*(sum_i+s)) dpi?=min(dpi?,dpj?+sumi??toti?+totn??s?totj??(sumi?+s))
去掉最小值,得到 d p i = d p j + s u m i ? t o t i + t o t n ? s ? t o t j ? ( s u m i + s ) dp_i = dp_j + sum_i*tot_i + tot_n*s - tot_j*(sum_i+s) dpi?=dpj?+sumi??toti?+totn??s?totj??(sumi?+s)
移項,得到 d p j = t o t j ? ( s u m i + s ) + d p i ? s u m i ? t o t i ? t o t n ? s dp_j = tot_j*(sum_i+s) + dp_i - sum_i*tot_i - tot_n*s dpj?=totj??(sumi?+s)+dpi??sumi??toti??totn??s
在 t o t j tot_j totj? 為橫坐標, d p j dp_j dpj? 為縱坐標的平面直角坐標系中,
這是一條 y = ( s + s u m i ) ? x + d p i ? s u m i ? t o t i ? t o t n ? s y = (s+sum_i) * x + dp_i - sum_i * tot_i - tot_n * s y=(s+sumi?)?x+dpi??sumi??toti??totn??s 的直線。
寫成 y = k x + b y = kx+b y=kx+b 的形式, k = s + s u m i k = s+sum_i k=s+sumi? , b = d p i ? s u m i ? t o t i ? t o t n ? s b = dp_i-sum_i*tot_i-tot_n*s b=dpi??sumi??toti??totn??s.
由于 k k k 是定值,所求的 d p i dp_i dpi? 存在于 b b b 中,所以我們只需要找到最小的 b b b 即可。
如何尋找最小的 b b b ?
發現有一些點會出現在這條直線上,我們把這樣的點稱為 決策點,即 ( t o t j , d p j ) (tot_j,dp_j) (totj?,dpj?)。
對于這些決策點,由于 k k k 是定值,所以有且只有一條 k = s + s u m i k=s+sum_i k=s+sumi? 的直線經過一個決策點,這些決策點一共會產生不超過 j j j 條直線。
對于已知的一個決策點 ( t o t j , d p j ) (tot_j,dp_j) (totj?,dpj?),我們把它們帶入到一次函數表達式里去,就能解出一個 b b b ,枚舉 j j j 得到最小的 b b b 即可。
但這種方法過于樸素,時間復雜度不變。考慮優化。
由于我們是從決策點出發,推導 b b b 的值。則說明決策點坐標(或者說 j j j )與 b b b 之間存在線性關系。考慮決策點坐標之間的關系來優化。
對于三個決策點 ( t o t j 1 , d p j 1 ) , ( t o t j 2 , d p j 2 ) , ( t o t j 3 , d p j 3 ) (tot_{j_1},dp_{j_1}),(tot_{j_2},dp_{j_2}),(tot_{j_3},dp_{j_3}) (totj1??,dpj1??),(totj2??,dpj2??),(totj3??,dpj3??) (我們設這三點 j 1 < j 2 < j 3 j_1 < j_2 < j_3 j1?<j2?<j3? ,由于 t , c > 0 t,c > 0 t,c>0 ,所以這三點的橫坐標依次遞增,即 t o t j 1 < t o t j 2 < t o t j 3 tot_{j_1} < tot_{j_2} < tot_{j_3} totj1??<totj2??<totj3??)來說,當這三個決策點有且僅有取 ( t o t j 2 , d p j 2 ) (tot_{j_2},dp_{j_2}) (totj2??,dpj2??) 時, b b b 有最小值,那么這三點所構成的直線不會兩兩重合,并分為兩種情況:
情況 1 ( j 2 j_2 j2? 在 j 1 j_1 j1? 與 j 3 j_3 j3? 的連線上方)
當這三點構成一個向上凸出的形狀,即 上凸 。顯然此時 j 2 j_2 j2? 一定不會使得 b b b 取最小值,如下圖所示。
情況 2 ( j 2 j_2 j2? 在 j 1 j_1 j1? 與 j 3 j_3 j3? 的連線下方)
當這三點構成一個向下凸出的形狀,即 下凸 。顯然此時 j 2 j_2 j2? 可能使得 b b b 取最小值,如下圖所示。
發現只有 下凸 的情況 ( j 2 j_2 j2? 在 j 1 j_1 j1? 與 j 3 j_3 j3? 的連線下方) 才可能使 j 2 j_2 j2? 取到最小的 b b b 。
則有 d p j 2 ? d p j 1 t o t j 2 ? t o t j 1 < d p j 3 ? d p j 2 t o t j 3 ? t o t j 2 \frac{dp_{j_2}-dp_{j_1}}{tot_{j_2}-tot_{j_1}} < \frac{dp_{j_3}-dp_{j_2}}{tot_{j_3}-tot_{j_2}} totj2???totj1??dpj2???dpj1???<totj3???totj2??dpj3???dpj2??? 。
即直線 j 1 → j 2 j_1 \to j_2 j1?→j2? 的 k k k 小于 j 2 → j 3 j_2 \to j_3 j2?→j3? 直線的 k k k ,本質上是這兩條直線的斜率關系。
因此,我們需要維護 相鄰兩點間直線的 k k k (斜率) ,并當它們 單調遞增 時, j 2 j_2 j2? 所得到的 b b b 就可能是最小值。
那么什么時候 j 2 j_2 j2? 所取的 b b b 就一定是最小值呢?
我們發現,當一段單調遞增的 k k k 滿足一個點的左邊的 k ’ k’ k’ 都小于 k k k ,右邊的 k ’ k’ k’ 都大于 k k k 時,這個點就是使 b b b 最小的點。
如果我們只維護 相鄰兩點間連線斜率大于等于 k k k 的 k ′ k' k′ (斜率),那么在這個單調遞增的序列中最小值就能使 b b b 最小。
這不就是單調隊列的思路嗎?
總結一下:
- 我們用單調隊列維護相鄰兩點間直線的 k k k ,使其單調遞增。
- 在單調隊列里放的是 k k k 單調遞增的點的編號。
- 最終答案是單調隊列的隊頭坐標代入 d p i = d p j + s u m i ? t o t i + t o t n ? s ? t o t j ? ( s u m i + s ) dp_i = dp_j + sum_i*tot_i + tot_n*s - tot_j*(sum_i+s) dpi?=dpj?+sumi??toti?+totn??s?totj??(sumi?+s).
- 為了維護單調性,我們需要從左側隊頭開始刪除。即判斷隊頭斜率 d p q h e a d + 1 ? d p q h e a d t o t q h e a d + 1 ? t o t q h e a d ≤ s + s u m i \frac{dp_{q_{head+1}}-dp_{q_{head}}}{tot_{q_{head+1}}-tot_{q_{head}}} \leq s+sum_i totqhead+1???totqhead??dpqhead+1???dpqhead???≤s+sumi? 時,把隊頭出隊即可。 為了避免精度問題,且 t o t tot tot 有單調遞增性,那么我們不妨判斷 d p q h e a d + 1 ? d p q h e a d ≤ ( s + s u m i ) ? ( t o t q h e a d + 1 ? t o t q h e a d ) {dp_{q_{head+1}}-dp_{q_{head}}} \leq (s+sum_i) * ({tot_{q_{head+1}}-tot_{q_{head}}}) dpqhead+1???dpqhead??≤(s+sumi?)?(totqhead+1???totqhead??).
- 添加時,如果 q i q_i qi? 不能與前面的點滿足單調性,那么直接把前面的點不斷出隊,直到滿足單調性為止。即當 d p i ? d p q t a i l t o t i ? t o t q t a i l ≤ d p q t a i l ? d p q t a i l ? 1 t o t q t a i l ? t o t q t a i l ? 1 \frac{dp_{i}-dp_{q_{tail}}}{tot_{i}-tot_{q_{tail}}} \leq \frac{dp_{q_{tail}}-dp_{q_{tail-1}}}{tot_{q_{tail}}-tot_{q_{tail-1}}} toti??totqtail??dpi??dpqtail???≤totqtail???totqtail?1??dpqtail???dpqtail?1??? 時不斷出隊即可。同樣避免精度問題,判斷 ( d p i ? d p q t a i l ) ? ( t o t q t a i l ? t o t q t a i l ? 1 ) ≤ ( d p q t a i l ? d p q t a i l ? 1 ) ? ( t o t i ? t o t q t a i l ) ({dp_{i}-dp_{q_{tail}}}) * ({tot_{q_{tail}}-tot_{q_{tail-1}}}) \leq ({dp_{q_{tail}}-dp_{q_{tail-1}}})*({tot_{i}-tot_{q_{tail}}}) (dpi??dpqtail??)?(totqtail???totqtail?1??)≤(dpqtail???dpqtail?1??)?(toti??totqtail??) 即可。
時間復雜度 O ( n ) O(n) O(n) .
#include <bits/stdc++.h>
using namespace std;
const int N = 300005;
long long n,s,ans,t[N],c[N],dp[N],sum[N],tot[N];
long long q[N],head=1,tail=1;
int main()
{cin >> n >> s;for (int i=1;i<=n;i++){cin >> t[i] >> c[i];sum[i] = sum[i-1] + t[i];tot[i] = tot[i-1] + c[i];dp[i] = 1e18;}ans = 1e18;dp[0] = 0;for (int i=1;i<=n;i++){while (head < tail && dp[q[head+1]]-dp[q[head]] <= (s+sum[i])*(tot[q[head+1]]-tot[q[head]])) head++;dp[i] = dp[q[head]] + sum[i]*tot[i] + tot[n]*s - tot[q[head]]*(sum[i]+s);while (head < tail && (dp[i]-dp[q[tail]])*(tot[q[tail]]-tot[q[tail-1]]) <= (dp[q[tail]]-dp[q[tail-1]])*(tot[i]-tot[q[tail]])) tail--;q[++tail] = i;}cout<<dp[n];return 0;
}
為什么單調隊列初始
head = 1,tail = 1
,而不能寫作head = 1,tail = 0
?
考慮到還有dp[0] = 0
,要么把tail
設為head
,要么把tail
設為head-1
再在隊列里加入dp[0]
。
后記
斜率優化看起來好像的確比想象中抽象很多,希望對大家理解斜率優化有所幫助!