斜率優化DP是一種DP的一種優化方式,目的在於將一類具有單調性的DP優化為線性。
注:本文只適用於較為基礎的斜率優化DP,以便為初學者提供一個思路。這一類可以利用單調性線性或\(\log\)復雜度之內求解的DP問題統稱為1D/1D類型的動態規划。
斜率優化DP一般適用於的式子形式為\[DP[i]=a[j]+b(i,j)+c\]
\(a[j]\)表示一個只與j有關的常量,\(b(i,j)\) 表示一個與i,j同時有關的量,\(c\)是一個自帶的常量,與i和DP方程自帶的常量有關。
由於直接看式子會比較抽象,我們下面來看一道例題
例題
題目:[Usaco2008 Mar]土地購買
農夫John准備擴大他的農場,他正在考慮N (1 <= N <= 50,000) 塊長方形的土地. 每塊土地的長寬滿足(1 <= 寬 <= 1,000,000; 1 <= 長 <= 1,000,000). 每塊土地的價格是它的面積,但FJ可以同時購買多快土地. 這些土地的價格是它們最大的長乘以它們最大的寬, 但是土地的長寬不能交換. 如果FJ買一塊3×5的地和一塊5×3的地,則他需要付5×5=25. FJ希望買下所有的土地,但是他發現分組來買這些土地可以節省經費. 他需要你幫助他找到最小的經費.
輸入
第1行: 一個數: N
第2..N+1行: 第i+1行包含兩個數,分別為第i塊土地的長和寬
輸出
第一行: 最小的可行費用.
輸入樣例
500
輸出樣例
4
100 1
15 15
20 5
1 100
樣例提示
FJ分3組買這些土地: 第一組:100×1, 第二組1×100, 第三組20×5 和 15×15 plot. 每組的價格分別為100,100,300, 總共500.
解析
這道題的DP方程是顯然的,設\(x\)為長,\(y\)為寬。
由於對於一對\(i\),\(j\),當\(x[i]>x[j]\)且\(y[i]>y[j]\)時,我們買土地\(i\)就一定可以把土地\(j\)順帶買了,所以我們可把這些土地\(j\)找出來舍去,在DP時無需考慮
於是我們可以對土地按照\(x\)作為第一關鍵字,\(y\)作為第二關鍵字按遞增順序排序,然后遍歷一遍數組,去掉以上所述的一類沒有意義的土地\(j\),這時我們得到了一組按順序排好的土地,保證\(x\)遞增,\(y\)遞減。
這時我們得到了我們的DP方程,設\(f[i]\)表示前\(i\)個土地分好組,總共需要的最小代價。即\(f[i]=min(f[j]+y[j+1]x[i])\),我們對於每一個\(i\),枚舉前面每一個\(j\)作為決策點,這樣我們得到了一個最壞\(O(n^2)\)的方法,這樣顯然是跑不過的。
這時,我們需要利用這個式子的單調性進行優化。
Part 1
設\(k< j< i\),\(j\)、\(k\)為兩個可行的決策點,\(i\)為DP當前枚舉到的土地。這時,我們在\(i\)處進行決策,考慮在什么情況下,決策點為\(j\)比決策點為\(k\)更優(當然,你也可以討論決策點為\(k\)比決策點為\(j\)更優,但是到最后你會發現我們DP的順序決定了我們選擇決策點的順序)
這時\[f[j]+y[j+1]x[i] < f[k]+y[k+1]x[i]\]
我們對這個式子進行移項化簡\[f[j]-f[k] < -x[i](y[j+1]-y[k+1])\]
由於\(y\)具有單調減的單調性(注意每一次用到單調性,這是斜率優化之所以可行的核心),\(y[j+1]-y[k+1] < 0\),所以這個式子的最終形式是\[\frac{f[j]-f[k]}{y[j+1]-y[k+1]} > -x[i]\]
我們會發現以上的式子非常像一個斜率的計算,所以我們可以把每一個決策點x看作一個點\((y[x+1],f[x])\),我們比較兩個決策點誰更優就是在比較兩個點之間的連線的斜率與當前點\(i\)的\(-x[i]\)。
所以,在\(k\)、\(j\)連線的斜率大於\(-x[i]\)時,選擇決策點\(j\)比決策點\(k\)更優。而且由於\(x[i]\)具有單調增的單調性,\(-x[i]\)單調減,所以\(i\),\(i+1\),\(i+2\)…之后每一個點做決策時\(j\)都一定比\(k\)更優,所以我們可以直接把\(k\)省略掉,不再考慮。
Part 2
然后我們再來觀察另外一個式子,
仍然是\(k< j< i\),我們考慮在什么情況下\(j\)這個決策點是無意義的。
設\(a\)、\(b\)兩點連線的斜率為\(s(a,b)\),我們發現當\(s(k,j)>s(j,i)\)時,設當前決策點為\(t\)。則如果\(s(k,j) > -x[t]\),則\(k\)比\(j\)更優;而當\(s(k,j) < -x[t]\)時,\(s(j,i) < s(k,j) < -x[t]\),\(j\)比\(k\)更優,但同時\(i\)比\(j\)更優。所以無論如何,\(j\)永遠不可能成為最優決策點,我們把它省略掉。
Part 3
然后我們便發現,如果我們用一個雙端隊列把這些可能會被用到的決策點依次存起來(按DP的順序,也就是下標的從小到大或從大到小),根據我們上面所提到的,把不需要的決策點去掉,對留下來的點中任意相鄰三個點\(i、j、k\),一定有\(s(k,j) < s(j,i)\),也就是說,這個斜率是單調增的(或者對於有些題是單調減)。正如以下這個圖片:

現在我們簡單說一下如何維護這個單調隊列,\(j1\)、\(j2\)、\(j3\)....是單調隊列里當前正在維護的決策點,設我們的DP當前處理到了\(i\),也就是第\(i\)塊土地。這些決策點還在單調隊列里當且僅當這些點可能成為\(i\)、\(i+1\)、\(i+2\)...的最優決策點。
現在我們想要計算第\(i\)個點的\(f[i]\),首先我們要找出單調隊列里哪個點是\(i\)的最優決策點,我們從隊首開始看起,設隊首的兩個點為\(j1\)、\(j2\),我們計算\( s(j1,j2) \),當\( s(j1,j2) <= -x[t] )\),\(j2\)比\(j1\)更優,我們把\(j1\)從單調隊列中pop出來,以后也用不到了(見Part 1),然后再對\( s(j2,j3) \)進行比較;當\( s(j1,j2) > -x[t] \),\(j1\)比\(j2\)更優,且因為隊列的單調性(見Part 3),\( s(j2,j3) > s(j1,j2) > -x[t] )\),也就是說\(j1\)優於\(j2\)優於\(j3\)...,則\(j1\)是對於當前點\(i\)最優的一個決策。
然后有了決策點,我們就可以計算出\(f[i]\)。現在我們來考慮將\( (y[i+1],f[i]) \)添加到單調隊列中去。
假設我們要添加的點為\(j5\)(如上圖中紅點),設隊尾的兩個點為\(j3\)、\(j4\)。我們比較\( s(j3,j4) \)與\( s(j4,j5) \),若\( s(j3,j4) > s(j4,j5) \),則\(j4\)永遠不可能是最優決策點(見Part 2),把\(j4\),也就是隊尾的點彈出,再依次比較;若\( s(j3,j4) < s(j4,j5) \)滿足斜率單調增的單調性,把\(j5\),也就是DP到的當前節點插入到隊尾。
以上是對這個單調隊列的理解,一句話總結,你需要維護一個存儲決策點的隊列,這個隊列的相鄰兩個點的斜率單調。
注意
- 當隊列里的點個數小於兩個,你需要停止循環。
- 隊列里一般會先放一個所有參數均為0(\(f[0]\)....)的點0作為哨兵,有可能有參數不為0(具體看題,反正我沒有遇到過)。
代碼
1 /* Stay hungry, stay foolish. */ 2 #include<bits/stdc++.h> 3 using namespace std; 4 template <class T> inline void read(T &x) { 5 int t; bool flag=false; 6 while((t=getchar())!='-'&&(t<'0'||t>'9')) ; 7 if(t=='-') flag=true,t=getchar(); x=t-'0'; 8 while((t=getchar())>='0'&&t<='9') x=x*10+t-'0'; 9 if(flag) x=-x; 10 } 11 const int maxn=50010; 12 struct Node { 13 long long x,y; 14 bool operator < (Node B)const { 15 if(x==B.x) return y<B.y; 16 return x<B.x; 17 } 18 }p[maxn]; 19 long long n,f[maxn],q[maxn]; 20 double slope(long long a,long long b) { 21 return (1.0*(f[a]-f[b]))/(p[a+1].y-p[b+1].y); 22 } 23 int main() { 24 //freopen("acquire.in","r",stdin); 25 //freopen("acquire.out","w",stdout); 26 read(n); 27 for(int i=1;i<=n;++i) read(p[i].x),read(p[i].y); 28 sort(p+1,p+n+1); 29 int cnt=0; 30 for(int i=1;i<=n;++i) { 31 if(p[i].y<=p[i+1].y) continue; 32 while(cnt&&p[cnt].y<=p[i].y) --cnt; 33 p[++cnt]=p[i]; 34 } 35 //讀入並將多余的土地刪除 36 int h=0,t=1; 37 q[h]=0; //設置哨兵 38 for(int i=1;i<=cnt;++i) { 39 while(t-h>1&&slope(q[h],q[h+1])>=-p[i].x) ++h; //刪除隊首非最優決策點 40 f[i]=f[q[h]]+p[q[h]+1].y*p[i].x; //計算當前點DP值 41 while(t-h>1&&slope(q[t-2],q[t-1])<slope(q[t-1],i)) --t; //刪除隊尾非最優決策點 42 q[t++]=i; //把當前點插入隊尾 43 } 44 cout<<f[cnt]<<endl; 45 return 0; 46 }
題目推薦
以下推薦幾道看了本文可以去水掉的題:
