斜率優化一般使用單調隊列輔助進行實現,用於優化 \(DP\) 的時間複雜度,比較抽象,須要讀者有較高的數學素養。數組
本文例題連接函數
使用單調隊列優化 \(DP\) ,一般能夠解決型如: \(dp[i]=min(f(j))+g(i)\) 的狀態轉移方程。其中 \(f(i)\) 是隻關於 \(i\) 的函數, \(g(j)\) 是隻關於 \(j\) 的函數。樸素的解決方法是在第二層循環中枚舉 \(j\) 來實現最小值,時間複雜度爲 \(O(n^2)\) 。可使用單調隊列來維護這個最小值實現 \(O(n)\) 的時間複雜度。優化
而斜率優化利用上述方法進行改進,實現對於型如: \(dp[i]=min(f(i,j))+g(i)\) 的狀態轉移方程。對比第一種狀況,能夠發現函數 \(f\) 函數與兩個值 \(i,j\) 都有關,簡單地使用單調隊列是沒法優化的。這時候就開始引入主題斜率優化了。ui
下面結合一道例題來具體詳解。題目來自於 \(HNOI2008\) 省選題目。spa
有 \(n\) 個數字 \(C\),把它分爲若干組,給出另外一個數 \(L\) ,每組的花費爲\((i-j+\sum_{k=i}^jC_k-L)^2\),總花費爲全部組的花費之和。求最小總花費。code
先考慮樸素的 \(dp\) 作法。blog
設 \(dp[i]\) 爲將前 \(i\) 個數字分組後的最小花費。求和能夠考慮使用前綴和來優化,設前綴和數組爲 \(pre\) 。則狀態轉移方程能夠寫爲:隊列
\(dp[i]=Min(dp[j]+(sum[i]-sum[j])+(i-(j+1))-L)^2,0≤j<i)\)圖片
便是:get
\(dp[i]=Min(dp[j]+(sum[i]-sum[j]+i-j-L-1)^2,0≤j<i)\)
那麼 \(sum\) 數組能夠初始化爲:
for(int i = 1; i <= n; i++) { Quick_Read(val[i]); sum[i] = sum[i - 1] + val[i]; }
設 \(pre[i]=sum[i]+i\) ,再進一步設 \(l=L+1\) 那麼狀態轉移方程能夠寫爲:
\(dp[i]=Min(dp[j]+(pre[i]-pre[j]-l)^2,0≤j<i)\)
狀態轉移
int Get_Dp(int i, int j) { return dp[j] + (pre[i] - pre[j] - l) * (pre[i] - pre[j] - l); }
若枚舉 \(j\) ,則時間複雜度爲 \(O(n)^2\) ,時間複雜度不優。使用斜率優化能夠對其進行優化。
假設當前枚舉到 \(i\) ,須要獲得 \(i\) 的狀態。假設有兩個決策點 \(j\) , \(k\) ,知足決策點 \(j\) 優於決策點 \(k\) 。用符號語言能夠表達爲:
\(dp[j]+(pre[i]-pre[j]-l)^2<dp[k]+(pre[i]-pre[k]-l)^2\)
展開得:
\(dp[j]+pre[i]^2+pre[j]^2+l^2-2\times pre[i]\times pre[j]-2\times l\times pre[i]+2\times l\times pre[j]<dp[k]+pre[i]^2+pre[k]^2+l^2-2\times pre[i]\times pre[k]-2\times l\times pre[i]+2\times l\times pre[k]\)
進一步整理得 :
\(dp[j]+pre[j]^2-dp[k]-pre[k]^2<(pre[i]-l)\times 2\times (pre[j] - pre[k])\)
觀察可得:左邊的式子只與 \(j\) 和 \(k\) 有關,但右邊的式子還與 \(i\) 有關。也能夠發現若知足上述式子,則會有 \(j\) 優於 \(k\) 。再分類討論:
得到分子的函數:
int Get_Up(int j, int k) { return dp[j] + pre[j] * pre[j] - dp[k] - pre[k] * pre[k]; }
得到分母的函數:
int Get_Down(int j, int k) { return pre[j] - pre[k]; }
有了上述的一級結論,能夠進一步推導出二級結論:
設 \(x,y\) 的斜率表示爲 \(k(x,y)\) 。若存在三點 \(a,b,c\) ,有 \(k(a,b)>k(b,c)\) ,便是圖像造成上凸的形狀時,那麼點 \(b\) 絕對不是最優的。
分類討論:
那麼就能夠得出答案的點必須知足 \(k(a_1,a_2)<k(a_2,a_3)<...<k(a_{m-1},a_m)\) 。所有呈現出下凸狀態,以下圖。
這樣下標遞增,斜率遞增的點集可使用單調隊列來維護。
找出當前最優的點爲 \(que[head]\) ,即隊頭元素。
while(Get_Up(que[head + 1], que[head]) <= 2 * (pre[i] - l) * Get_Down(que[head + 1], que[head]) && head < tail) head++;
用當前點 \(i\) 來更新隊列,使得該隊列呈下凸之勢。
while(Get_Up(que[tail], que[tail - 1]) * Get_Down(i, que[tail]) >= Get_Up(i, que[tail]) * Get_Down(que[tail], que[tail - 1]) && head < tail) tail--;
按照上述方法進行狀態轉移,獲得的 \(dp[n]\) 就是當前的最優解。
代碼比較短,一鼓作氣。(注意要開 \(long\) \(long\))
#include <cstdio> #define int long long void Quick_Read(int &N) { N = 0; int op = 1; char c = getchar(); while(c < '0' || c > '9') { if(c == '-') op = -1; c = getchar(); } while(c >= '0' && c <= '9') { N = (N << 1) + (N << 3) + (c ^ 48); c = getchar(); } N *= op; } void Quick_Write(int N) { if(N < 0) { putchar('-'); N = -N; } if(N >= 10) Quick_Write(N / 10); putchar(N % 10 + 48); } const int MAXN = 5e5 + 5; int dp[MAXN]; int pre[MAXN], val[MAXN]; int n, l; int que[MAXN]; int head, tail; int Get_Dp(int i, int j) { return dp[j] + (pre[i] - pre[j] - l) * (pre[i] - pre[j] - l); } int Get_Up(int j, int k) { return dp[j] + pre[j] * pre[j] - dp[k] - pre[k] * pre[k]; } int Get_Down(int j, int k) { return pre[j] - pre[k]; } void Line_Dp() { head = 1; tail = 1; for(int i = 1; i <= n; i++) { while(Get_Up(que[head + 1], que[head]) <= 2 * (pre[i] - l) * Get_Down(que[head + 1], que[head]) && head < tail) head++; dp[i] = Get_Dp(i, que[head]); while(Get_Up(que[tail], que[tail - 1]) * Get_Down(i, que[tail]) >= Get_Up(i, que[tail]) * Get_Down(que[tail], que[tail - 1]) && head < tail) tail--; que[++tail] = i; } Quick_Write(dp[n]); } void Read() { Quick_Read(n); Quick_Read(l); l++; for(int i = 1; i <= n; i++) { Quick_Read(val[i]); pre[i] = pre[i - 1] + val[i] + 1; } } signed main() { Read(); Line_Dp(); return 0; }