這篇文章使用markdown 和 latex 寫成。博客園對markdown的支持不是太完善,若是顯示異常,請刷新頁面php
之前寫過一篇關於動態規劃斜率優化的文章,可是很是很差懂T_T,這兩天作了一些斜率優化的題,再總結一下:node
首先這個題樸素的DP方程是這樣的:
\(f_i=min(f_j+\sum_{k=j+1}^i(cost_k)+M\)
若是咱們記\(cost\)的前綴和爲\(s\),那麼
$f_i=min(f_j+(s_i-s_j)^2)+M $
化簡獲得:
\(f_i=min(f_j+s_i^2+s_j^2-2s_is_j)+M\)
注意到\(s_i\)只與\(f_i\)有關,因此能夠從括號內提出
\(f_i=min(f_j+s_j^2-2s_is_j)+M+s_i^2\)
因此決策的表達式就是
\(f_j+s_j^2-2s_is_j\)ios
如今咱們考慮任意兩個決策點\(a\)和\(b\)(也就是說\(j=a\)和\(j=b\)的狀況)
假設\(a < b\)
那麼決策\(a\)比決策\(b\)更優的條件就是
\(f_a+s_a^2-2s_is_a < f_b+s_b^2-2s_is_b\)
整理獲得
\((f_a+s_a^2)-(f_b+s_b^2) < 2s_i(s_a-s_b)\)
繼續整理,獲得markdown
\[\frac{(f_a+s_a^2)-(f_b+s_b^2)}{s_a-s_b} < 2s_i\]優化
這就是決策\(a\)比決策\(b\)更優的條件
仔細觀察這個式子,若是咱們把\(f_a+s_a^2\)和\(f_b+s_b^2\)分別看作點A和B的縱座標,\(s_a\)和\(s_b\)看作點A和B的橫座標,那麼不等式左面就能夠當作是一個斜率式。斜率優化的名字就由此而來。
下文中咱們就把不等式左面記作\(k_{a,b}\)spa
咱們再來考慮三個決策\(a,b,c(a < b < c)\).
若是有 \(k_{a,b} < k_{b,c}\) 那麼意味着什麼呢?
判斷兩個決策誰更優須要和\(s_i\)比較,咱們分三種狀況討論:code
經過以上三種狀況,咱們發現只要有 \(k_{a,b} < k_{b,c}\),決策\(b\)就必定不是最好的,不用考慮了
基於這一點,咱們有效地減小了須要考慮的決策數,從而對這類DP進行了優化。隊列
那麼具體怎麼實現呢?get
若是咱們把各個決策以點\((s_j,f_j+s_j^2)\)的形式畫在平面上,而且對於任意三個點A,B,C(按照橫座標A < B < C)都保證\(k_{a,b} < k_{b,c}\) 不成立(換句話說咱們刪去全部使得\(k_{a,b} < k_{b,c}\)的B點),那麼咱們就會發現剩下的圖形是一個凸多邊形。博客
也就是說,若是把各個決策當作是點,咱們實際上要維護的是這些點的凸包。
具體實現的時候,分兩種狀況:
1.像這道題同樣,決策點是依次出現的(橫座標依次增大),那麼就很是簡單了:
咱們維護一個單調隊列,每次算完一個\(f\)的值,就把其對應的決策點加入單調隊列隊尾,把這個決策點看作是C,若是單調隊列中有兩個點或以上,就把最後的兩個點看作是A和B,若是\(k_{a,b} < k_{b,c}\)那麼就把單調隊列中最後一個點刪去,直到\(k_{a,b} < k_{b,c}\) 不成立爲止,加入這個新的決策點
每次須要計算\(f_i\)值的時候,首先維護一下隊頭,若是隊列中有兩個或以上元素,且第一個點和第二個點的斜率值 $ < s_i\(的話,就刪去隊頭。*由於\)s_i\(是遞增的,如今\) < s_i\(之後確定\) < s_{i+1}$,因此這樣維護是合理的。*
這樣維護事後,直接取隊頭的決策就是當前最優的決策。
若是你想不通爲何隊頭就是最優決策的話,畫一張圖看看。由於維護過的圖形是一個上凸包,因此全部的斜率都是隨着橫座標的增大而遞減的,又由於第一個斜率$ < s_i\(,因此全部的斜率都\) < s_i\(。那麼從隊頭開始每一個點都優於他後面一個點(由於這個斜率\) < s_i$),根據傳遞性隊頭的點就是最優的了。
這種狀況下狀態數仍然是\(O(n)\)的,但轉移的時間複雜度從\(O(n)\)降低到\(O(1)\)。又由於單調隊列均攤下倆是\(O(1)\)的,因此總體時間複雜度就從\(O(n^2)\)優化到了\(O(n)\)
2.若是各個決策點出現的順序是無須的,好比bzoj1492,那麼就不能簡單的用一個單調隊列維護了,咱們須要一顆平衡樹,每次找到決策須要插入的位置,並分別向左向右維護凸包。這變成了一個經典的動態凸包問題。固然,這個題有其餘不用斜率優化的更好的作法。
以上就是斜率優化的所有原理和實現方法。附上hdu3507的代碼以供參考:
#include <cstdio> #include <cstdlib> #include <cstring> #include <algorithm> #include <iostream> #define MAXN (500000+10) using namespace std; int s[MAXN],a[MAXN],f[MAXN]; struct node{ int x,y,ss; node(int x=0,int y=0,int ss=0):x(x),y(y),ss(ss) {} }; struct Mono_queue{ node t[MAXN]; int f,r; int size; void init(){ memset(t,0,sizeof t); f=0; r=0; size=0; } void push(node x){ while (size>=2){ if ((x.x-t[r-1].x >0 && t[r-1].x-t[r-2].x>0) || (x.x-t[r-1].x <0 && t[r-1].x-t[r-2].x<0)){ if ((x.y-t[r-1].y)*(t[r-1].x-t[r-2].x)<=(t[r-1].y-t[r-2].y)*(x.x-t[r-1].x)) r-- , size--; else break; }else { if ((x.y-t[r-1].y)*(t[r-1].x-t[r-2].x)>=(t[r-1].y-t[r-2].y)*(x.x-t[r-1].x)) r-- , size--; else break; } } t[r++]=x; size++; } void maintain(int x){ while (size>=2){ if ((t[f+1].x-t[f].x)>0){ if ((t[f+1].y-t[f].y)<=x*(t[f+1].x-t[f].x)) f++ , size--; else break; }else{ if ((t[f+1].y-t[f].y)>=x*(t[f+1].x-t[f].x)) f++ , size--; else break; } } } int top(){ return t[f].ss; } }T; int main (int argc, char *argv[]) { int m,n; while (scanf("%d%d",&n,&m)!=EOF){ T.init(); memset(f,0,sizeof f); for (int i=1;i<=n;i++) scanf("%d",&a[i]); for (int i=1;i<=n;i++) s[i]=s[i-1]+a[i]; T.push(node(0,0,0)); for (int i=1;i<=n;i++){ T.maintain(2*s[i]); int ss=T.top(); f[i]=f[ss]+s[ss]*s[ss]-2*s[ss]*s[i]+s[i]*s[i]+m; T.push(node(s[i],f[i]+s[i]*s[i],i)); } printf("%d\n",f[n]); } return 0; }
斜率優化的題目都是大同小異,DP 的形式都差很少,只要能整理成斜率式,就能斜率優化。若是變量分離不開,那就不能斜率優化了。