【學習筆記】動態規划—斜率優化DP(超詳細)
\(update\ 2020.6.19:\) 臨近退役,終於來修鍋啦QAQ(更正基礎概念上的錯誤;\(\text{Latex}\) 規范化;重新排版;增加標題號;添加【關於單調性的研究】;添加 \(\text{CDQ}\) 維護斜率優化的例子)
【學習筆記】動態規划—各種 \(\text{DP}\) 優化
【前言】
第一次寫這么長的文章。
寫完后對斜優的理解又加深了不少(\(update\ 2020.6.19:\) 回過頭來看這句話滿是諷刺啊,明明這時候連基本的概念都沒有理清....)。
本文講解較詳,只要耐心讀下去,相信大部分 \(\text{OIer}\) 都能看懂。
斜率優化 \(\text{dp}\),顧名思義就是利用斜率相關性質對 \(\text{dp}\) 進行優化。
斜率優化通常可以由兩種方式來理解,需要靈活地運用數學上的數形結合,線性規划思想。
對於這樣形式的 \(\text{dp}\) 方程:\(dp[i]=Min/Max(a[i]∗b[j]+c[j]+d[i])\),其中 \(b\) 嚴格單調遞增。
該方程的關鍵點在於 \(a[i]*b[j]\) 這一項,它既有 \(i\) 又有 \(j\),於是單調隊列優化不再適用,可以嘗試使用斜率優化。
一.【理解方式】
以 【模板】 玩具裝箱 \(\text{toy [P3195]}\) 為例,兩種斜優的理解方式。
設 \(S[n]=\sum_{i=1}^n(C[i]+1)\),用 \(dp[i]\) 表示裝好前 \(i\) 個的最小花費,則轉移方程為:\(dp[i]=min(dp[j]+(S[i]−S[j]-1-L)^2)\)。
為方便描述,將 \(\text{L}\) 提前加 \(1\),再把 \(min\) 去掉,得到狀態轉移方程:\(dp[i]=dp[j]+(S[i]−S[j]-L)^2\)。
化簡得:\(dp[i]=S[i]^2-2S[i]L+dp[j]+(S[j]+L)^2-2S[i]S[j]\)
1.【代數法(數形結合)】
只含 \(\text{L}\) 的項對於每一個 \(i\) 的擇優篩選過程都是完全一樣的值,只含 \(Function(i)\) 的項在一次 \(i\) 的擇優篩選過程中不變,含 \(Function(j)\) 的項可能會不斷變化(在本題中表現為為嚴格單增)。
我們以此為划分依據,把同類型的項用括號括起來,即:\(dp[i]=(-2S[i]S[j])+(dp[j]+(S[j]+L)^2)+(S[i]^2-2S[i]L)\)
(1).【維護一個凸包】
設 \(j_1,j_2\) \((0 \leqslant j_1<j_2<i)\) 為 \(i\) 的兩個決策點,且滿足決策點 \(j_2\) 優於 \(j_1\),
有:\((-2S[i]S[j_2])+(dp[j_2]+(S[j_2]+L)^2)+(S[i]^2-2S[i]L) \leqslant (-2S[i]S[j_1])+(dp[j_1]+(S[j_1]+L)^2)+(S[i]^2-2S[i]L)\)
即:\((-2S[i]S[j_2])+(dp[j_2]+(S[j_2]+L)^2) \leqslant (-2S[i]S[j_1])+(dp[j_1]+(S[j_1]+L)^2)\)
划重點:此處移項需要遵循的原則是:參變分離。將 \(Function(i)\) 視作未知量,用 \(Function(j)\) 來表示出 \(Function(i)\) 。
移項得:\(-2S[i](S[j_2]-S[j_1]) \leqslant (dp[j_1]+(S[j_1]+L)^2)-(dp[j_2]+(S[j_2]+L)^2)\)
\(\because C[j] \geqslant 1\)
\(\therefore S[j+1] > S[j]\)
\(又 \because j_2 > j_1\)
\(\therefore S[j_2]-S[j_1]>0\)
\(\therefore 2S[i] \geqslant \frac {(dp[j_2]+(S[j_2]+L)^2)-(dp[j_1]+(S[j_1]+L)^2)} {S[j_2]-S[j_1]}\)
設 \(Y(j)=dp[j]+(S[j]+L)^2,X(j)=S[j]\),
即 \(2S[i] \geqslant \frac {Y(j_2)-Y(j_1)} {X(j_2)-X(j_1)}\)
顯然等式右邊是一個關於點 \(P(j_2)\) 和 \(P(j_1)\) 的斜率式,其中 \(P(j)=(X(j),Y(j))=(S[j],dp[j]+(S[j]+L)^2)\)。
也就是說,如果存在兩個決策點 \(j_1,j_2\) 滿足 \((0 \leqslant j_1<j_2<i)\),使得不等式 \(\frac {Y(j_2)-Y(j_1)} {X(j_2)-X(j_1)} \leqslant 2S[i]\) 成立,或者說 使得 \(P(j_2),P(j_1)\) 兩點所形成直線的斜率小於等於 \(2S[i]\),那么決策點 \(j_2\) 優於 \(j_1\)。
划重點:斜優靈活多變,細節麻煩也多,所以盡量將問題模式化。
比如這里的最終公式,盡量化為 \(\frac {(j)-(j')} {(j)-(j')}\) 的形式,而不是 \(\frac {(j)-(j')} {(j')-(j)}\) ,雖然直接做一般也不會出什么問題,但這樣子可以方便理解,方便判斷凸包方向等等。
假設有醬紫的三個點 \(P(j_1),P(j_2),P(j_3)\),\(k_1,k_2\) 為斜率,如下圖所示情況(三點分別為 \(A,B,C\)):
顯然有 \(k_2 < k_1\)。設 \(k_0=2S[i]\),由上述結論可知:
\((a).\) 若 \(k_1 \leqslant k_0\),則 \(j_2\) 優於 \(j_1\) 。反之,若 \(k_1 > k_0\),則 \(j_1\) 優於 \(j_2\) 。
\((b).\) 若 \(k_2 \leqslant k_0\),則 \(j_3\) 優於 \(j_2\) 。反之,若 \(k_2 > k_0\),則 \(j_2\) 優於 \(j_3\) 。
於是這里可以分三種情況來討論:
\((1).\) \(k_0 < k_2 < k_1\)。由 \((a),(b)\) 可知:\(j_1\) 優於 \(j_2\) 優於 \(j_3\) 。
\((2).\) \(k_2 \leqslant k_0 < k_1\)。由 \((a),(b)\) 可知:\(j_1\) 和 \(j_3\) 均優於 \(j_2\)。
\((3).\) \(k_2 < k_1 \leqslant k_0\)。由 \((a),(b)\) 可知:\(j_3\) 優於 \(j_2\) 優於 \(j_1\) 。
可以發現,對於這三種情況,\(j_2\) 始終不是最優解,於是我們可以將 \(j_2\) 從候選決策點中踢出去(刪除),只留下 \(j_1\) 和 \(j_3\),刪后的情況如下圖所示:
我們要對某一個問題的解決方案進行優化改進,無非就是關注兩個要點:正確性和高效性(很多時候高效性都體現為單調性)。
醬紫做的正確性是毋庸置疑的,因為在 \(j_1\) 和 \(j_3\) 其中必定有一個比 \(j_2\) 更優,所以刪除 \(j_2\) 對答案沒有任何影響。
那么高效性呢?自己在腦子里面 \(yy\) 一下,在一個坐標系的第一象限中(本題中 \(X(j)\) 和 \(Y(j)\)均大於等於 \(0\),至於為什么這里要說等於,下面會提到),有若干個離散的點,任取三點,如果左邊斜率大於右邊斜率,則形成了上述情況,必定會刪點,因而消除這種情況。所以將最后留下來的點首位相連,其形成的各個線段斜率從左到右必定是單調遞增的(有可能非嚴格遞增,這個問題之后再討論)。
如果學習過計算幾何相關知識,會意識到這個過程其實與求凸包算法是類似的。(順手丟一個廣告:【學習筆記】計算幾何全家桶)
實際上在圖中選取最靠左下面、下面、右下面的點首位相連,就是最后留下來的點了,它們形成了一個下凸包,即凸包(又名凸殼)的下半部分(不嚴謹的講,給定二維平面上的點集,凸包就是將最外層的點連接起來構成的凸多邊形,它能包含點集中所有的點——摘自百度百科)。
維護出的圖形如下圖所示:
可以嘗試在凸包圍起來的區域內任意取一點,其必定能在包圍圈上找到兩個點使得該點可被刪除。如上 \(\text{L}\) 點,它與 \(D,E\) 兩點形成了一個可刪點圖形。
注意:圖中 \(C,D,E\) 故意畫成了三點一線,而實際上點 \(D\) 是可以刪去的,且嚴格凸包不允許存在這種情況。關於去重的細節問題后面會提到。
同理,如果把不等式 \(\frac {Y(j_2)-Y(j_1)} {X(j_2)-X(j_1)} \leqslant k_0[i]\) 改為 \(\frac {Y(j_2)-Y(j_1)} {X(j_2)-X(j_1)} \geqslant k_0[i]\),那么維護出來的就是一個上凸包。
(2).【尋找最優決策點】
在一次決策點的尋找中,易知下凸包點集里總會存在一點,使得它與左鄰點形成的斜率小於等於 \(k_0\) ,與右鄰點形成的斜率大於 \(k_0\) 。
例如上圖中的 \(E\) 點,設 \(k_4 \leqslant k_0 < k_5\) 由於凸包上面的斜率呈單增態,那么有:\(k_1 < k_2 < k_3 < k_4 \leqslant k_0 < k_5 < k_6 < k_7\),所以決策點 \(E\) 優於其他所有點,即 \(E\) 就是 \(dp[i]\) 的最優決策點。
如果暴力查找的話,就是從第一個點開始向后掃描,找到第一個斜率大於 \(k_0\) 的線段,其左端點即為最優決策點。由於凸包上的斜率依次遞增,可以二分快速得到這個點。
2.【線性規划】
先回顧一下模板題的 \(\text{dp}\) 方程:\(dp[i]=S[i]^2-2S[i]L+dp[j]+(S[j]+L)^2-2S[i]S[j]\)。
對其進行移項變化,變成形如 \(y=kx+b\) 的點斜式。
划重點:移項要遵循的原則是:把含有 \(function(i)*function(j)\) 的表達式看作斜率 \(k_0\) 乘以未知數 \(x\),含有 \(dp[i]\) 的項必須要在 \(b\) 的表達式中,含有 \(function(j)\) 的項必須在 \(y\) 的表達式中。如果未知數 \(x\) 的表達式單調遞減,最好讓等式兩邊同乘個 \(-1\),使其變為單增。
至於為什么說要讓 \(x\) 的表達式單增,emm...其實是為了讓一些較簡單的問題模式化,不易出錯,如果你非要單減,可以嘗試倒序枚舉,至於是否正確,具體實現需要注意的玄學問題等等,因為覺得太麻煩沒有細想,我也不清楚會遇到什么問題。
例如此題,原 \(\text{dp}\) 方程可化為:
\((2S[i])S[j]+(dp[i]-S[i]^2+2S[i]L)=(dp[j]+(S[j]+L)^2)\)
其中 \(k_i=2S[i],\) \(x_i=S[j],\) \(b_i=dp[i]-S[i]^2+2S[i]L,\) \(y_i=dp[j]+(S[j]+L)^2\)。
其實也可以化為:
\((2S[i])(S[j]+L)+(dp[i]-S[i]^2)=(dp[j]+(S[j]+L)^2)\)
其中 \(k_i=2S[i],\) \(x_i=S[j]+L,\) \(b_i=dp[i]-S[i]^2,\) \(y_i=dp[j]+(S[j]+L)^2\)。
還可以化為 \(...\)
\(...\)
只要滿足上述移項原則,對答案是沒有任何影響的。
這里以第一種形式為例,先畫出草圖:
(1).【高中數學知識】
我們的目的是求出一個最優決策點 \(j\) 使得 \(dp[i]\) 最小,又因為 \(b[i]=dp[i]-S[i]^2\) ,所以就是要找到某個點使這條直線經過它時算出來的 \(b\) 最小,即是高中數學課本上的線性規划問題。
(2).【尋找最優決策點】
如圖所示,點 \(E\) 即為最優決策點。顯然,這個使得 \(b\) 最小的最優決策點位於下凸包點集中,且與上述代數法求得的點一致。
3.【兩種思考方式的結合】
強烈推薦用線性規划思想主導思考過程,因為圖形的變幻較直觀,更重要的是:在某某變量不滿足單調性時,通過圖形可以迅速做出判斷並改變策略(在后面【關於單調性的研究】中會詳細解釋)。
而代數法通常在不便於識別方程特征時起一個轉換思維方向的作用,因為有些題可能會直接出現 \(\frac{Y(j)-Y(j')}{X(j)-X(j')}\) 的形式,需要通過一系列代數推導后再繪草圖模擬決策。
二.【維護凸包】
實際上只要讓維護的凸包方向相同,兩種思考方式的代碼是一模一樣的。
用單調隊列維護凸包點集,操作分三步走:
\((1).\) 進行擇優篩選時,在凸包上找到最優決策點 \(j\) 。
\((2).\) 用最優決策點 \(j\) 更新 \(dp[i]\) 。
\((3).\) 將 \(i\) 作為一個決策點加入圖形並更新凸包(如果點 \(i\) 也是 \(dp[i]\) 的決策點之一,則需要將 \((3)\) 換到最前面)。
在本題中步驟 \((3)\) 的具體操作為:判斷當隊尾的點與點 \(i\) 形成可刪點圖形時,出隊直至無法再刪點,然后將 \(i\) 加入隊列。
在判斷可刪圖形時有兩種方法(以 下凸包 為例),一種是 slope(Q[t-1],Q[t])<=slope(Q[t],i)
,另一種是 slope(Q[t-1],Q[t])<=slope(Q[t-1],i)
,都表示出現了可以刪去點 \(Q[t]\) 的情況(只要對邊界、去重的處理足夠嚴謹,兩種寫法是沒有區別的)。其中 \(Q\) 是維護凸包點集的隊列。
該做法時間復雜度為 \(O(n\log n)\),瓶頸在於二分尋找最優決策點。
三.【再優化】
運用決策單調性進行優化。決策單調性相關基礎知識見 【學習筆記】動態規划—各種 \(\text{DP}\) 優化,這里只放一下定義:
設 \(j_0[i]\) 表示 \(dp[i]\) 轉移的最優決策點,那么決策單調性可描述為 \(\forall i\leqslant i',j_0[i]\leqslant j_0[i']\)。也就是說隨着 \(i\) 的增大,所找到的最優決策點是遞增態(非嚴格遞增)。
(1).【決策單調性證明】
還是以 玩具裝箱 為例,來簡單證一波決策單調性,方法采用四邊形不等式。
顯然,本題的轉移方程呈現出了 \(dp[i]=min(dp[j]+w(i,j))\) 的形式,即 \(1D/1D\) 動態規划方程,其中 \(w(i,j)=(S[i]−S[j]-1-L)^2\)。
\(證明:設\) \(Q=S[i]−S[j]-1-L\)
\(\therefore w(i,j)=(S[i]−S[j]-1-L)^2=Q^2\)
\(\begin{aligned} \therefore w(i+1,j+1)=&(S[i+1]−S[j+1]-1-L)^2\\ =&((S[i]+C[i+1]+1)−(S[j]+C[j+1]+1)-1-L)^2\\ =&(Q+C[i+1]-C[j+1])^2 \end{aligned}\)
\(\begin{aligned} w(i,j+1)=&(S[i]−S[j+1]-1-L)^2\\ =&(S[i]−(S[j]+C[j+1]+1)-1-L)^2\\ =&(Q-C[j+1]-1)^2 \end{aligned}\)
\(\begin{aligned} w(i+1,j)=&(S[i+1]−S[j]-1-L)^2\\ =&((S[i]+C[i+1]+1)−S[j]-1-L)^2\\ =&(Q+C[i+1]+1)^2 \end{aligned}\)
\(\therefore w(i,j)+w(i+1,j+1)=2Q^2+2C[i+1]Q-2C[j+1]Q+C[i+1]^2-2C[i+1]C[j+1]+C[j+1]^2\)
\(\therefore w(i+1,j)+w(i,j+1)=2Q^2+2C[i+1]Q-2C[j+1]Q+C[i+1]^2+2C[i+1]+2C[j+1]+C[j+1]^2+2\)
\(\therefore w(i,j)+w(i+1,j+1)-w(i+1,j)+w(i,j+1)=-2(C[i+1]+1)(C[j+1]+1)\)
\(又 \because C[i],C[j] \geqslant 1\)
\(\therefore -2(C[i+1]+1)(C[j+1]+1) \leqslant -8\)
\(\therefore w(i,j)+w(i+1,j+1) \leqslant w(i+1,j)+w(i,j+1)\)
四邊形不等式成立,所以此方程具有決策單調性。
證畢。
(2).【單調隊列】
由於最優決策點遞增,可以用單調隊列對其進行維護。操作 \((2),(3)\) 不需要改動,操作 \((1)\) 改為:判斷當隊首的第一根線段斜率小於等於 \(k_0[i]\) 時就出隊,直至斜率大於 \(k_0[i]\),此時的隊首即為最優決策點。
正確性顯然。因為隨着 \(i\) 的變大,最優決策點 \(j_0[i]\) 也會跟着變大,如果已知某個點在當前情況下不夠侑秀,那么在這之后也一定不會作為最優決策點,所以可以直接出隊。
時間復雜度為 \(O(n)\) 。
(3).【再證決策單調性】
一樣的,兩種思路。
先觀察 \(k_0[i]\) 的表達式:\(k_0[i]=2S[i]\) ,明顯在本題中 \(k_0\) 呈單增態。
【代數法】
\(k_0[i]\) 遞增就說明我們找到的第一個斜率大於 \(k_0[i]\) 的線段在不斷地向后移,也就是說,如果我們找到了某一個最優決策點 \(j\),那么在下一次決策中,最優決策點 \(j'\) 必定在 \(j\) 的后面。
決策單調性得證。
【線性規划】
畫出草圖:
直線 \(Line_i\) 的斜率 \(k_0[i]\) 遞增,
由圖可知最優決策點在遞增。
決策單調性得證。
【其他】
從這個角度來看的話,貌似決策單調性和 \(X(j),k_0[i]\) 的單調性是相通的?
於是一個結論就出現了:如果 \(X(j),k_0[i]\) 均單調不減,則該方程必定有決策單調性(自己瞎 \(yy\) 的,不敢肯定一定正確)。
四.【Code】
這道題 \(...\) 數據太水了 \(...\) 我一開始 \(\text{L}\) 忘了加 \(1\) 居然還過了 \(...\)
#include<cstring>
#include<cstdio>
#define LL long long
#define Re register LL
const int N=5e4+5;
LL i,j,n,L,h=1,t=0,Q[N],S[N],dp[N];
//S[n]=∑C[i]+1, dp[i]=min(dp[j]+(S[i]-(S[j]+L+1))^2),++L
//dp[i]=S[i]^2-2*S[i]*L+dp[j]+(S[j]+L)^2-2S[i]*S[j]
//(2*S[i]) * S[j] + (dp[i]-S[i]^2+2S[i]L)=(dp[j]+(S[j]+L)^2)
// k * x + b = y
inline LL min(Re a,Re b){return a<b?a:b;}
inline LL X(Re j){return S[j];}
inline LL Y(Re j){return dp[j]+(S[j]+L)*(S[j]+L);}
inline long double slope(Re i,Re j){return (long double)(Y(j)-Y(i))/(X(j)-X(i));}//記得開long double
int main(){
scanf("%lld%lld",&n,&L);++L;
for(i=1;i<=n;S[i]+=S[i-1]+1,++i)scanf("%lld",&S[i]);
Q[++t]=0;//重中之重
for(i=1;i<=n;++i){
while(h<t&&slope(Q[h],Q[h+1])<=2*S[i])++h;//至少要有兩個元素 h<t。出隊判斷時盡量加上等號
dp[i]=dp[j=Q[h]]+(S[i]-S[j]-L)*(S[i]-S[j]-L);
while(h<t&&slope(Q[t-1],Q[t])>=slope(Q[t-1],i))--t;//至少要有兩個元素 h<t。入隊判斷時盡量加上等號
Q[++t]=i;
}
printf("%lld",dp[n]);
}
五.【各種玄學問題】
(ノ°ο°)ノ前方高能預警 (*°ω°*)ノ"非戰斗人員請撤離!! *・_・)ノ
\((1).\) 寫出 \(\text{dp}\) 方程后,要先判斷能不能使用斜優,即是否存在 \(function(i)*function(j)\) 的項或者 \(\frac{Y(j)-Y(j')}{X(j)-X(j')}\) 的形式。
\((2).\) 通過大小於符號或者 \(b\) 中 \(dp[i]\) 的符號結合題目要求 \((min/max)\) 判斷是上凸包還是下凸包,不要見一個方程就直接盲猜一個下凸。
\((3).\) 當 \(X(j)\) 非嚴格遞增時,在求斜率時可能會出現 \(X(j_1)=X(j_2)\) 的情況,此時最好是寫成這樣的形式:return Y(j)>=Y(i)?inf:-inf
,而不要直接返回 \(inf\) 或者 \(-inf\),在某些題中情況較復雜,如果不小心畫錯了圖,返回了一個錯誤的極值就完了,而且這種錯誤只用簡單數據還很難查出來。
\((4).\) 注意比較 \(k_0[i]\) 和 \(slope(j_1,j_2)\) 要寫規范,要用右邊的點減去左邊的點進行計算(結合 \((3)\) 來看,可防止返回錯誤的極值),如果用的代數法理解,寫出了 (X(j2)-X(j1))*k0<=Y(j2)-Y(j1)
或 (X(j2)-X(j1))*k0<=Y(j2)-Y(j1)
,而恰巧 \(j_1,j_2\) 又寫反了,便會出現等式兩邊同除了負數卻沒變號的情況。當然用 \(k_0\) 和 \(\frac {Y(j_2)-Y(j_1)}{X(j_2)-X(j_1)}\) 進行比較是沒有這種問題的。
\((5).\) 隊列初始化大多都要塞入一個點 \(P(0)\),比如 玩具裝箱 \(\text{toy}\),需要塞入 \(P(S[0],dp[0]+(S[0]+L)^2)\) 即 \(P(0,0)\),其代表的決策點為 \(j=0\)。
\((6).\) 手寫隊列的初始化是 h=1,t=0
,由於塞了初始點導致 \(t\) 加 \(1\),所以在一些題解中可以看到 h=t=1
甚至是 h=t=0
,h=t=2
之類的寫法,其實是因為省去了塞初始點的代碼。它們都是等價的。
\((7).\) 手寫隊列判斷不為空的條件是 h<=t
,而出入隊判斷都需要有至少 \(2\) 兩個元素才能進行操作。所以應是 h<t
。
\((8).\) 計算斜率可能會因為向下取整而出現誤差,所以 \(slope\) 函數最好設為 \(long\) \(double\) 類型。
\((9).\) 有可能會有一部分的 \(\text{dp}\) 初始值無法轉移過來,需要手動提前弄一下,例如 擺渡車 \(\text{[P5017]}\)。
\((10).\) 在比較兩個斜率時,盡量寫上等於,即 <=
和 >=
而不是 <
和 >
。這樣寫對於去重有奇效(有重點時會導致斜率分母出鍋),但不要以為這樣就可以完全去重,因為要考慮的情況可能會非常復雜,所以還是推薦加上 \((3)\) 中提到的特判,確保萬無一失。
六.【關於單調性的研究】
划重點:注意是否具有單調性,不要盲目地使用單調隊列進行維護。
(1).【X(j) 單增與單減】
將方程變為 \(\frac {Y(j_2)-Y(j_1)} {X(j_2)-X(j_1)} \leqslant k_0[i]\) 或者 \(\frac {Y(j_2)-Y(j_1)} {X(j_2)-X(j_1)} \geqslant k_0[i]\) 或者 \(kx+b=y\) 的形式,變化要遵循之前提到的原則,尤其是 \(X(j)\) 的單調性,結合圖形會更好理解(目的是將單減的形式變為單增,方便維護)。
(2).【決策點橫坐標 X(j) 不單調】
注意:\(X(j)\) 的單調性會影響凸包維護方式的選擇。
如果決策點橫坐標 \(X(j)\) 不存在單調性該怎么辦(既不單增也不單減)?
(假設此時 \(k_0[i]\) 仍然單調)
此時維護凸包就不能用單調隊列了,因為插入點時可能會插到凸包點集中間的某個位置,而隊列是不支持這種操作的,需要用到平衡樹維護或者用 \(\text{CDQ}\) 分治提供單調性(后面會講到)。
這里有計算幾何基礎的話會更易理解,因為上面維護圖形時的刪點操作與水平序 \(\text{Graham}\) 掃描法求凸包是類似的,而掃描法的前提為:點集呈水平序,即點從左至右依次排列(體現為 \(X(j)\) 單調不減)。
(3).【待決策點斜率 K0[i] 不單調】
注意:\(k_0[i]\) 的單調性會影響尋找最優決策點方式的選擇。
如果斜率 \(k_0[i]\) 不存在單調性該怎么辦?
(假設此時 \(X(j)\) 仍然單調)
我們仍可以用隊列維護凸包點集,但不知道每一次會在什么地方取得最優決策點,所以必須要保留整個凸包以確保決策有完整的選擇空間,也就是說不能彈走隊首,同時查找答案也不能直接取隊首,只能使用二分。
- 【模板】 任務安排 \(3\)(可以證明該題不具有決策單調性)
(4).【X(j) 與 K0[i] 均不單調】
現在來看 \(k_0[i]\) 與 \(X(j)\) 均不單調的情況:
此時無法再用隊列維護凸包了,但平衡樹本就支持查詢前驅、后繼,直接把 \(k_0[i]\) 丟進去詢問即可。
而 \(\text{CDQ}\) 就更有意思了:在 \((2)\) 中做法的基礎上恰好還能再加一維偏序!
我們直接人為地排出單調性,像普通單調隊列那樣維護就可以了(代碼放后面)。
七.【例題】
1.【預處理DP 初始值】
去年 \(noip\) 普及組的題(霧)。
【Code】
#include<cstdio>
#define Re register int
const int N=4e6+105;
int i,j,n,m,h=1,t=0,T,ti,ans=2e9,G[N],S[N],Q[N],dp[N];
inline int min(Re a,Re b){return a<b?a:b;}
// i
//dp[i]=min(dp[j]+ ∑(i-T[k]))
// k=j+1
//dp[i]=dp[j]+(G[i]-G[j])*i-(S[i]-S[j])
//(i) * G[j] + (dp[i]+S[j]-i*G[i]) = (dp[j]+S[j])
// k * x + b = y
inline int X(Re j){return G[j];}
inline int Y(Re j){return dp[j]+S[j];}
inline long double slope(Re i,Re j){return X(i)==X(j)?(Y(j)>Y(i)?2e9:-2e9):(long double)(Y(j)-Y(i))/(long double)(X(j)-X(i));}
int main(){
scanf("%d%d",&n,&m);
for(i=1;i<=n;T=-min(-T,-ti),++G[ti],S[ti]+=ti,++i)scanf("%d",&ti);
for(i=1;i<T+m;++i)G[i]+=G[i-1],S[i]+=S[i-1];
for(i=0;i<m;++i)dp[i]=G[i]*i-S[i];//提前處理dp初始值
Q[++t]=0;
for(i=m;i<T+m;++i){
while(h<t&&slope(Q[h],Q[h+1])<=i)++h;
dp[i]=dp[j=Q[h]]+(G[i]-G[j])*i-S[i]+S[j];
while(h<t&&slope(Q[t-1],Q[t])>=slope(Q[t-1],i-m+1))--t;
Q[++t]=i-m+1;//(j+1)+m<=i
}
for(i=T;i<T+m;++i)ans=min(ans,dp[i]);
printf("%d",ans);
}
2.【單調隊列+二分】
【題目描述】
有 \(N\) 個任務等待完成(順序不得改變),這 \(N\) 個任務被分成若干批,每批包含相鄰的若干任務。從時刻 \(0\) 開始,這些任務被分批加工,第 \(i\) 個任務單獨完成所需的時間是 \(T_i\) 。只有一台機器,在每批任務開始前,機器需要啟動時間 \(S\),完成這批任務所需的時間是各個任務需要時間的總和(同一批任務將在同一時刻完成)。每個任務的費用是它的完成時刻乘以它的費用系數 \(F_i\)。請確定一個分組方案,使得總費用最小。
【數據范圍】
\(T1:\) \(1 \leqslant N \leqslant 5000, 0 \leqslant S \leqslant 50,1 \leqslant T_i, F_i \leqslant 100\)
\(T2:\) \(1 \leqslant N \leqslant 10000, 0 \leqslant S \leqslant 50,1 \leqslant T_i, F_i \leqslant 100\)
\(T3:\) \(1 \leqslant N \leqslant 300000, 1 \leqslant S\leqslant 512,0 \leqslant F_i \leqslant 512,|T_i| \leqslant 512\)
(1).【T1】
設 \(ST[i]=\sum_{j=1}^i T[j],SF[i]=\sum_{j=1}^i F[j]\)
\(\text{dp}\) 方程很簡單:\(dp[p][i]=min(dp[p-1][j]+(ST[i]+p\times S)(SF[i]-SF[j]))\),但是 \(O(n^3)\) 的時間復雜度連 \(T1\) 都過不了。
由於不知道每一次分段之前已經分了多少,所以需要用一維空間和一層循環來表示這個信息,從而知道 \(S\) 需要乘以多少。
那么可以反過來,用一種名為費用提前計算的經典思想來進行優化(據說這個叫未來 \(\text{dp}\)),每分出一批任務,那么對於這之后的每一個任務都需要多出一個 \(S\) 的時間,所以可以直接計算 \(S\) 對后面的影響。
即:\(dp[i]=min(dp[j]+ST[i](SF[i]-SF[j])+S(SF[n]-SF[j]))\)
壓成了 \(O(n^2)\) 后,\(T1\) 就可以 \(\text{AC}\) 了,但它還能繼續優化。
(2).【T2】
先轉化為斜率式看看?
\((S+ST[i]) * SF[j] + (dp[i]-ST[i]*SF[i]-S\times SF[i]) = (dp[j])\)
其中 \(k=S+ST[i],\) \(x=SF[j],\) \(b=dp[i]-ST[i]\times SF[i]-S\times SF[i],\) \(y=dp[j]\) 。
決策點要使得 \(dp[i]\) 盡量小,且 \(S+ST[i]\) 和 \(SF[j]\) 都嚴格單增,所以直接用單調隊列維護一個下凸包即可。
時間復雜度為 \(O(n)\) 。
【Code】
#include<cstring>
#include<cstdio>
#define LL long long
#define Re register LL
const int N=1e4+5;
LL i,j,n,h=1,t=0,S,Q[N],ST[N],SF[N],dp[N];
//dp[p][i]=min(dp[p-1][j]+(ST[i]+S*p)*(SF[i]-SF[j]));
//dp[i]=dp[j]+ST[i]*(SF[i]-SF[j])+S*(SF[n]-SF[j]);
//(S+ST[i]) * SF[j] + (dp[i]-ST[i]*SF[i]-S*SF[i]) = (dp[j])
// k * x + b = y
inline LL min(Re a,Re b){return a<b?a:b;}
inline LL X(Re j){return SF[j];}
inline LL Y(Re j){return dp[j];}
inline long double slope(Re i,Re j){return (long double)(Y(j)-Y(i))/(X(j)-X(i));}
int main(){
scanf("%lld%lld",&n,&S);
for(i=1;i<=n;ST[i]+=ST[i-1],SF[i]+=SF[i-1],++i)scanf("%lld%lld",&ST[i],&SF[i]);
Q[++t]=0;
for(i=1;i<=n;++i){
while(h<t&&slope(Q[h],Q[h+1])<(S+ST[i]))++h;
dp[i]=dp[j=Q[h]]+ST[i]*(SF[i]-SF[j])+S*(SF[n]-SF[j]);
while(h<t&&slope(Q[t-1],Q[t])>slope(Q[t-1],i))--t;
Q[++t]=i;
}
printf("%lld",dp[n]);
}
(3).【T3】
因 \(F_i\) 可等於 \(0\),\(X(j)\) \((\) 即 \(SF[i])\) 非嚴格遞增,所以需要特判 \(X(j_1)=X(j_2)\) 的情況(但仍具有單調性,可以使用隊列維護凸包)。
因 \(T_i\) 可小於 \(0\),\(k_0[i](\) 即 \(S+ST[i])\) 無單調性,所以不具有決策單調性,可以用四邊形不等式進行證明:
該 \(\text{dp}\) 方程顯然為 \(dp[i]=dp[j]+w(i,j)\) 的形式,其中 \(w(i,j)=ST[i](SF[i]-SF[j])+S(SF[n]-SF[j])\) 。
\(\text{證明:}\) \(\text{設}\) \(Q=S(SF[n]-SF[j])\)
\(\therefore w(i,j)=ST[i](SF[i]-SF[j])+Q\)
\(\begin{aligned} \therefore w(i+1,j+1)=&ST[i+1]SF[i+1]-ST[i+1]SF[j+1]+S(SF[n]-SF[j+1])\\ =&ST[i+1]SF[i+1]-SF[j+1]*(ST[i]+T[i+1])+Q-S\times F[j+1]\\ \end{aligned}\)
\(\begin{aligned} w(i,j+1)=&ST[i](SF[i]-SF[j+1])+Q-S\times F[j+1]\\ =&ST[i]SF[i]-ST[i]SF[j+1]+Q-S\times F[j+1]\\ \end{aligned}\)
\(\begin{aligned} w(i+1,j)=&ST[i+1](SF[i+1]-SF[j])+Q\\ =&ST[i+1]SF[i+1]-ST[i+1]SF[j]+Q\\ =&ST[i+1]SF[i+1]-ST[i]SF[j]-T[i+1]SF[j]+Q \end{aligned}\)
\(\therefore w(i,j)+w(i+1,j+1)=ST[i](SF[i]-SF[j])+ST[i+1]SF[i+1]-SF[j+1](ST[i]+T[i+1])+2Q-S\times F[j+1]\)
\(\therefore w(i+1,j)+w(i,j+1)=ST[i]SF[i]-ST[i]SF[j+1]+ST[i+1]SF[i+1]-ST[i]SF[j]-T[i+1]SF[j]+2Q-S\times F[j+1]\)
\(\therefore w(i,j)+w(i+1,j+1)-w(i+1,j)+w(i,j+1)=-F[j+1]*T[i+1]\)
\(\text{又} \because 0 \leqslant F_i \leqslant 512,-512 \leqslant T_i \leqslant 512\)
\(\therefore \text{當} T_i \leqslant 0 時,w(i,j)+w(i+1,j+1) \geqslant w(i+1,j)+w(i,j+1)\)
\(\text{當} T_i \geqslant 0 \text{時},w(i,j)+w(i+1,j+1) \leqslant w(i+1,j)+w(i,j+1)\)
四邊形不等式不一定成立,所以此題不具有決策單調性。
證畢。
此時需要在隊列中二分查找最優決策點。
時間復雜度為 \(O(n\log n)\) 。
【Code】
#include<cstring>
#include<cstdio>
#define LL long long
#define Re register LL
const int N=3e5+5;
LL i,j,n,h=1,t=0,S,Q[N],ST[N],SF[N],dp[N];
//dp[p][i]=min(dp[p-1][j]+(ST[i]+S*p)*(SF[i]-SF[j]));
//dp[i]=dp[j]+ST[i]*(SF[i]-SF[j])+S*(SF[n]-SF[j]);
//(S+ST[i]) * SF[j] + (dp[i]-ST[i]*SF[i]-S*SF[i]) = (dp[j])
// k * x + b = y
//ti可小於0,所以ST[i]非遞增,只可二分
//fi可等於0,所以SF[i](X)非嚴格遞增,因此需要特判X(i)==X(j)的情況
inline LL min(Re a,Re b){return a<b?a:b;}
inline LL X(Re j){return SF[j];}
inline LL Y(Re j){return dp[j];}
inline long double slope(Re i,Re j){return X(j)==X(i)?(Y(j)>=Y(i)?1e18:-1e18):(long double)(Y(j)-Y(i))/(X(j)-X(i));
}//由於需要二分查找,多了一些限制:隊列里不能有在同一位置的點,返回inf還是-inf都影響着是否刪除重點,平時不可不管,二分必須注意返回值
inline LL sakura(Re k){
if(h==t)return Q[h];
Re l=h,r=t;
while(l<r){
Re mid=l+r>>1,i=Q[mid],j=Q[mid+1];
if(slope(i,j)<k)l=mid+1;
// if( (Y(j) - Y(i)) < k * (X(j) - X(i)) )l=mid+1;//注意是(j)-(i)因為Q[mid+1]>Q[mid]s即j>i即SF[j]>SF[i]即X(j)>X(i),如果是(i)-(j)的話乘過去要變號
else r=mid;
}
return Q[l];
}
int main(){
scanf("%lld%lld",&n,&S);
for(i=1;i<=n;ST[i]+=ST[i-1],SF[i]+=SF[i-1],++i)scanf("%lld%lld",&ST[i],&SF[i]);
Q[++t]=0;
for(i=1;i<=n;++i){
j=sakura(S+ST[i]);
dp[i]=dp[j]+ST[i]*(SF[i]-SF[j])+S*(SF[n]-SF[j]);
while(h<t&&slope(Q[t-1],Q[t])>=slope(Q[t-1],i))--t;//此處取等號作用出現,如果不取等,會WA第12個點
Q[++t]=i;
}
printf("%lld",dp[n]);
}
3.【CDQ/平衡樹】
因為暫時沒找到 \(X(j)\) 不單調、\(k_0[i]\) 單調的例題,這里直接講兩者均不單調的情況。
如果學習了動態凸包算法,會發現這其實就是套了個板子上去(平衡樹代碼較毒瘤就不放了)。
\(\text{CDQ}\) 做法也比較顯然,但因為遞歸過程不好描述,直接看代碼注釋吧。
時間復雜度為 \(O(n\log n)\)。
【Code】
#include<algorithm>
#include<cstring>
#include<cstdio>
#define LD long double
#define LL long long
#define Re register int
#define S2(a) (1ll*(a)*(a))
using namespace std;
const LL N=1e5+3,inf=1e18;
int n,H[N],W[N],Q[N];LL S[N],dp[N];
inline void in(Re &x){
int f=0;x=0;char c=getchar();
while(c<'0'||c>'9')f|=c=='-',c=getchar();
while(c>='0'&&c<='9')x=(x<<1)+(x<<3)+(c^48),c=getchar();
x=f?-x:x;
}
//dp[i]=min(dp[i],dp[j]+(H[i]-H[j])*(H[i]-H[j])+S[i-1]-S[j]);
//dp[i]=dp[j]-2*H[i]*H[j]+H[j]*H[j]+H[i]*H[i]+S[i-1]-S[j]
//(2*H[i]) * H[j] + (dp[i]-S[i-1]-H[i]*H[i]) = (dp[j]+H[j]*H[j]-S[j])
// k * x + b = y
#define X(j) (a[j].x)
#define Y(j) (a[j].y)
struct QAQ{
int k,x,id;LL y;
inline bool operator<(const QAQ &O)const{return x!=O.x?x<O.x:y<O.y;}
}a[N],b[N];
inline bool cmp(QAQ A,QAQ B){return A.k<B.k;}
inline LD slope(Re i,Re j){return X(i)==X(j)?(Y(j)>Y(i)?inf:-inf):(LD)(Y(j)-Y(i))/(X(j)-X(i));}
inline void CDQ(Re L,Re R){
if(L==R){Re j=a[L].id;a[L].y=dp[j]+(LL)H[j]*H[j]-S[j];return;}//此時dp[j]必定已經求出來了,直接算計Y(j)即可
Re mid=L+R>>1,p1=L,p2=mid+1,h=1,t=0;
for(Re i=L;i<=R;++i)a[i].id<=mid?b[p1++]=a[i]:b[p2++]=a[i];//按照i的大小分到左右兩邊(兩邊的k0[i]分別遞增)
for(Re i=L;i<=R;++i)a[i]=b[i];
CDQ(L,mid);//處理完左邊后,左邊的X(j)是遞增的,此時右邊還沒處理,所以右邊k0[i]是遞增的
for(Re i=L;i<=mid;++i){//把左邊的點拿出來維護凸包(使用單調隊列)
while(h<t&&slope(Q[t-1],Q[t])>=slope(Q[t-1],i))--t;
Q[++t]=i;
}
for(Re i=mid+1,j,id;i<=R;++i){//把右邊的點拿來決策(依舊是單調隊列)
while(h<t&&slope(Q[h],Q[h+1])<=a[i].k)++h;
if(h<=t)id=a[i].id,j=Q[h],dp[id]=min(dp[id],a[j].y-(LL)a[i].k*a[j].x+S[id-1]+(LL)H[id]*H[id]);
}
CDQ(mid+1,R);//處理完右邊后,兩邊都按照X(j)排好了序
Re w=L-1;p1=L,p2=mid+1;//把兩邊按照X(j)從小到大歸並起來
while(p1<=mid&&p2<=R)b[++w]=a[p1]<a[p2]?a[p1++]:a[p2++];
while(p1<=mid)b[++w]=a[p1++];while(p2<=R)b[++w]=a[p2++];
for(Re i=L;i<=R;++i)a[i]=b[i];
}
int main(){
// freopen("123.txt","r",stdin);
in(n);
for(Re i=1;i<=n;++i)in(H[i]);
for(Re i=1;i<=n;++i)in(W[i]);
for(Re i=1;i<=n;++i)S[i]=S[i-1]+W[i],dp[i]=inf;
for(Re i=1;i<=n;++i)a[i].k=(H[i]<<1),a[i].x=H[i],a[i].id=i;
sort(a+1,a+n+1,cmp);//先按k0[i]排序
dp[1]=0,CDQ(1,n);//注意左邊界上的點要單獨求
printf("%lld\n",dp[n]);
}
4.【題目鏈接】
(以此處為分界線,上面都是 \(X(j)\) 與 \(k_0[i]\) 均單調的例子)
-
任務安排 \(3\) \(\text{[SDOI2012] [P5785]}\) \(\text{[Loj10186]}\) \(\text{[Bzoj2726]}\)(\(X(j)\) 單調 \(k_0[i]\) 不單調)
-
高速公路 \(\text{[P3994]}\)(\(X(j)\) 單調 \(k_0[i]\) 不單調。樹上轉移)
-
購票 \(\text{[NOI2014] [P2305]}\)(\(X(j)\) 單調 \(k_0[i]\) 不單調。樹上轉移)
-
\(\text{Building Bridges [CEOI2017] [P4655]}\)(\(X(j)\) 與 \(k_0[i]\) 均不單調)
-
貨幣兌換 \(\text{[NOI2007] [P4027]}\)(\(X(j)\) 與 \(k_0[i]\) 均不單調)
【參考資料】
(本文部分內容摘自以下文章)