【學習筆記】動態規划—各種 DP 優化
【大前言】
個人認為貪心,\(dp\) 是最難的,每次遇到題完全不知道該怎么辦,看了題解后又瞬間恍然大悟(TAT)。這篇文章也是花了我差不多一個月時間才全部完成。
【進入正題】
用動態規划解決問題具有空間耗費大、時間效率高的特點,但也會有時間效率不能滿足要求的時候,如果算法有可以優化的余地,就可以考慮時間效率的優化。
【DP 時間復雜度的分析】
\(DP\) 高時間效率的關鍵在於它減少了“冗余”,即不必要的計算或重復計算部分,算法的冗余程度是決定算法效率的關鍵。而動態規划就是在將問題規模不斷縮小的同時,記錄已經求解過的子問題的解,充分利用求解結果,避免了反復求解同一子問題的現象,從而減少“冗余”。
但是,一個動態規划問題很難做到完全消除“冗余”。
下面給出動態規划時間復雜度的決定因素:
時間復雜度 \(=\) 狀態總數 \(×\) 每個狀態轉移的狀態數 \(×\) 每次狀態轉移的時間
【DP 優化思路】
一:減少狀態總數
\((1).\) 改進狀態表示
\((2).\)選擇適當的規划方向
二:減少每個狀態轉移的狀態數
\((1).\) 四邊形不等式和決策的單調性
\((2).\) 決策量的優化
\((3).\) 合理組織狀態
\((4).\) 細化狀態轉移
三:減少狀態轉移的時間
\((1).\) 減少決策時間
\((2).\) 減少計算遞推式的時間
(上述內容摘自 《動態規划算法的優化技巧》毛子青 ,想要深入了解其思想的可以去看看這篇寫得超級好的論文。)
看到這里是不是已經感覺有點蒙了呢?
本蒟蒻總結了一個簡化版本:
【DP 三要點】
在推導 \(dp\) 方程時,我們時常會感到毫無頭緒,而實際上 \(dp\) 方程也是有跡可循的,總的來說,需要關注兩個要點:狀態,決策和轉移。其中 “狀態” 又最為關鍵,決策最為復雜。
【狀態】
關於 “狀態” 的優化可以從很多角度出發,思維難度及其高,有時候狀態選擇的好壞會直接導致出現暴零和滿分的分化。
【決策】
與 “狀態” 不同,“決策” 優化則有着大量模板化的東西,在各大書籍,文章上你都可以看到這樣的話:只要是形如 \(XXX\) 的狀態轉移方程,都可以用 \(XXX\) 進行優化。
【轉移】
“轉移” 則指由最優決策點得到答案的轉移過程,其復雜度一般較低,通常可以忽略,但有時也需要特別注意並作優化。
本文將會重點針對 “決策” 優化部分作一些總結,記錄自己的感悟和理解。
一:【矩陣優化 DP】
\(updata\) 之后由於篇幅過大,已搬出。。。。。
補充:其實質是優化 “轉移”。
二:【數據結構優化 DP】
【前言】
在一些 \(dp\) 方程的狀態轉移過程中,我們通常需要在某個范圍內進行擇優,選出最佳決策點,這往往可以作為 \(dp\) 優化的突破口。
數據結構的使用較靈活,沒有一個特定的模板,需要根據具體情況而定,選擇合適的方案。由於狀態轉移總是伴隨着區間查詢最值,區間求和等操作,即動態區間操作,所以平衡樹可以作為一個有用的工具,但考慮到代碼復雜度,使用樹狀數組或者線段樹將會是一個不錯的選擇。。
其實質是優化 “決策”。
1.【維護合適的信息】
以 \(The\) \(Battle\) \(of\) \(Chibi\) \([UVA12983]\) 為例,大概題意就是計算在給定的序列中嚴格單調遞增子序列的數量,並對 \(1e9 +7\) 取模,給定序列長度小於等於 \(1000\) 。
方程應該是比較好推的,用 \(dp[i][j]\) 表示由序列中在 \(j\) 之前的數構成並以 \(a[j]\) 結尾的子序列中,長度為 \(i\) 的子序列的數量。則:\(dp[i][j]=\sum dp[i-1][k]\) ,其中 \(k < j\) \(且\) \(a[k] < a[j]\) 。
對於決策點 \(dp[i-1][k]\) 這里出現了 \(3\) 個信息:
\((1).\) 在原序列中的位置 \(k<j\) 。
\((2).\) \(a[k]<a[j]\) 。
\((3).\) \(dp[i-1][k]\) 的和。
對於 \((1)\),可以用枚舉的順序解決,剩下的兩個信息即是數據結構需要維護的信息。
對於每一次 \(dp[i]\) 的決策,可以將 \(a[j]\) 作為數據結構維護的關鍵字, \(dp[i-1][j]\) 作為權值,加入 \(-inf\) 后離散化,得到一個大小為 \(N+1\) 的數組並在上面建立樹狀數組,每次計算 \(dp[i][j]\) 時查詢前面已經加入的且關鍵字小於 \(a[j]\) 的 \(dp[i-1][k]\) 總和(即區間查詢),然后把 \(dp[i-1][j]\) 加入樹狀數組(單點查詢)。
時間復雜度為 \(O(logn)\)。
當問題涉及到的操作更復雜時,樹狀數組無法維護所需要的信息,就只有用線段樹了。這道題較簡單,所以選擇了代碼復雜度更低的樹狀數組。
2.【Code】
#include<algorithm>
#include<cstring>
#include<cstdlib>//UVA抽風,加上這個就好了
#include<cstdio>
#define Re register int
using namespace std;//UVA抽風,還要加這個
const int N=1005,P=1e9+7;
int n,m,T,k,ans,cnt,a[N],b[N],C[N],dp[N][N];
inline void add(Re x,Re v){while(x<=n+1)(C[x]+=v)%=P,x+=x&-x;}
inline int ask(Re x){Re ans=0;while(x)(ans+=C[x])%=P,x-=x&-x;return ans%P;}
int main(){
scanf("%d",&T);
for(Re o=1;o<=T;++o){
scanf("%d%d",&n,&m),ans=0,cnt=n;
memset(dp,0,sizeof(dp));
for(Re i=1;i<=n;++i)scanf("%d",&a[i]),b[i]=a[i],dp[1][i]=1;
sort(b+1,b+n+1);//離散
cnt=unique(b+1,b+n+1)-b-1;//去重
for(Re i=2;i<=m;++i){
memset(C,0,sizeof(C));//每次都要清空,重新開始維護
for(Re j=1;j<=n;++j)
dp[i][j]=ask((k=lower_bound(b+1,b+cnt+1,a[j])-b)-1),add(k,dp[i-1][j]);
}
for(Re j=1;j<=n;++j)(ans+=dp[m][j])%=P;
printf("Case #%d: %d\n",o,ans);
}
}
3.【題目鏈接】
【簡單題】
- \(The\) \(Battle\) \(of\) \(Chibi\) \([UVA12983]\)
【 標簽】動態規划/樹狀數組
【高檔題】
-
方伯伯的玉米田 \([P3287]\)
【 標簽】動態規划/二維樹狀數組 -
基站選址 \([P2605]\)
【 標簽】動態規划/線段樹
三:【決策單調性】
【前言】
形如 \(dp[i]=min(dp[j]+w(j,i))\) \((L_i \leqslant j \leqslant R_i)\) 的 \(dp\) 方程被稱作 \(1D/1D\) 動態規划,其中 \(L_i\) 和 \(R_i\) 單調遞增,\(w(j,i)\) 決定着優化策略選擇。
針對決策點具有的特性,可以大大降低尋找最優決策點的時間。
其實質是優化 “決策”。
1.【定義】
【決策單調性】
設 \(j_0[i]\) 表示 \(dp[i]\) 轉移的最優決策點,那么決策單調性可描述為 \(\forall i \leqslant j,j_0[i] \leqslant j_0[j]\)。也就是說隨着 \(i\) 的增大,所找到的最優決策點是遞增態(非嚴格遞增)。
【四邊形不等式】
\(w(x,y)\) 為定義在整數集合上的一個二元函數,若 \(\forall a \leqslant b \leqslant c \leqslant d,w(a,c)+w(b,d) \leqslant w(a,d)+w(b,c)\),那么函數 \(w\) 滿足四邊形不等式。
為什么叫做四邊形不等式呢?在 \(w(x,y)\) 函數的二維矩陣中挖一塊四邊形,左上角 加 右下角 小於等於 左下角 加 右上角。
2.【定理及其證明】
定理 (1):四邊形不等式的另一種定義
\(w(x,y)\) 為定義在整數集合上的一個二元函數,若 \(\forall a < b,w(a,b)+w(a+1,b+1) \leqslant w(a+1,b)+w(a,b+1)\),那么函數 \(w\) 滿足四邊形不等式。
\(證明:\)
\(\because \forall a < c,w(a,c)+w(a+1,c+1) \leqslant w(a+1,c)+w(a,c+1)\)
\(\therefore \forall a+1 < c,w(a+1,c)+w(a+2,c+1) \leqslant w(a+2,c)+w(a+1,c+1)\)
\(上下兩式相加,有:\) \(w(a,c)+w(a+2,c+1) \leqslant w(a,c+1)+w(a+2,c)\)
\(以此類推\) \(\forall a \leqslant b\leqslant c,w(a,c)+w(b,c+1) \leqslant w(a,c+1)+w(b,c)\)
\(同理\) \(\forall a \leqslant b \leqslant c \leqslant d,w(a,c)+w(b,d) \leqslant w(a,d)+w(b,c)\)
定理 (2):決策單調性
\(1D/1D\) 動態規划具有決策單調性當且僅當函數 \(w\) 滿足四邊形不等式 時成立。
\(證明:\)
\(\because\) \(j_0[i]\) \(在\) \(dp[i]\) \(的決策點中最優\)
\(\therefore\) \(\forall i \in [1,N],\forall j \in [0,j_0[i]-1],dp[j_0[i]]+w(j_0[i],i) \leqslant dp[j]+w(j,i)\)
\(易知\) \(\forall i' \in [i+1,N]\),\(均滿足\) \(j<j_0[i]<i<i'\)。
\(又\) \(\because\) \(函數\) \(w\) \(滿足四邊形不等式\)
\(\therefore\) \(w(j,i)+w(j_0[i],i') \leqslant w(j,i')+w(j_0[i],i)\)
\(移項得:\) \(w(j_0[i],i')-w(j_0[i],i) \leqslant w(j,i')-w(j,i)\)
\(與第一個式子相加,有:\) \(dp[j_0[i]]+w(j_0[i],i') \leqslant dp[j]+w(j,i')\)
最后的式子含義是:把 \(j_0[i]\) 作為 \(dp[i']\) 的決策點,一定比小於 \(j_0[i]\) 的任意一個 \(j\) 都要更好。也就是說,\(dp[i’]\) 的最優決策點不可能小於 \(j_0[i]\) ,即 \(j_0[i'] \geqslant j_0[i]\),所以方程 \(dp\) 具有決策單調性。
3.【證明決策單調性】
這里以 玩具裝箱 \(toy\) \([P3195]\) 為例(因為這個比較好證 QAQ),先來證一波決策單調性。
設 \(S[n]=\sum _{i=1}^n (C[i]+1)\),用 \(dp[i]\) 表示裝好前 \(i\) 個的最小花費,則 \(dp\) 方程為:\(dp[i]=min(dp[j]+(S[i]-S[j]-1-L)^2)\)。
很明顯,這個方程是一個 \(1D/1D\) 動態規划方程,其中 \(w(i,j)=(S[i]-S[j]-1-L)^2\)。
(注意在四邊形不等式中的 \(j\) 不是 \(i\) 決策點,可以理解為 \(i’\)。而 \(w(i,j)\) 的值可以理解為是由已完成的狀態 \(dp[i]\) 轉移到 \(dp[j]\) 所帶有的附加價值)。
\(證明:設\) \(Q=S[i]-S[j]-1-L\)
\(\therefore w(i,j)=(S[i]-S[j]-1-L)^2=Q^2\)
\(\text{證明:設}\) \(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)=2X^2+2C[i+1]X-2C[j+1]X+C[i+1]^2-2C[i+1]C[j+1]+C[j+1]^2\)
\(\therefore w(i+1,j)+w(i,j+1)=2X^2+2C[i+1]X-2C[j+1]X+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)\)
\(\text{又} \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)\)
\(\therefore w(i,j)+w(i+1,j+1)=2X^2+2C[i+1]X-2C[j+1]X+C[i+1]^2-2C[i+1]C[j+1]+C[j+1]^2\)
\(\therefore w(i+1,j)+w(i,j+1)=2X^2+2C[i+1]X-2C[j+1]X+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)\)
由定理 \((1)\) 可知,函數 \(w\) 滿足四邊形不等式。
又由定理 \((2)\)可知, 方程 \(dp\) 具有決策單調性。
在實戰中,通常使用打表的形式來驗證 \(w\) 函數的遞變規律。
4.【實現方法】
(\(ps.\) 對於此處及以下語言不嚴謹處,大家可以認真思考並給予建議,待日后對其理解加深后再行修改。)
這里選擇不同的例題將二者分開講。
【二分棧】
二分棧,顧名思義就是二分加棧。
用棧維護單調的決策點,二分找到最優決策點。
以 \(Lightning\) \(Conductor\) \([P3515]\) 為例,題目大意就是在給定長度為 \(n\) 的序列 \(a\) 中,對於每一個 \(i\),找到最小的自然數 \(p_i\) 滿足對於任意的 \(j \in [1,n]\),均有 \(a_j \leqslant a_i+p_i-\sqrt{\left|i-j\right|}\) 。
把這個式子變下形:\(p_i \geqslant a_j-a_i+\sqrt{\left|i-j\right|}\) 。
即 \(p_i=max \{ a_j+\sqrt{\left|i-j\right|} \} -a_i\)
即 \(p_i = max \{ max\{a_j+\sqrt{i-j}\}(j \in [1,i]),max\{a_j+\sqrt{j-i}\}(j \in [i+1,n]) \}-a_i\)
可以發現里面兩個 \(max\) 可以視為同一個問題(只要把序列翻轉一下就可以了),所以只需要考慮求出對於每一個 \(i\) 的 \(max\{ a_j+\sqrt{i-j} \}\),其中 \(j \in [1,i]\)。
設 \(y_j=a_j+\sqrt{i-j}\)
那么我們會得到 \(n\) 個關於 \(i\) 函數,\(p_i=max\{ y_j \}\)。
將樣例畫出來,如圖:
可知當 \(i=4\) 時,直線 \(x=4\) 與 \(y1=a_1+\sqrt{x-1}\) 的交點即為 \(p_4\)。
在上圖中,對於任意 \(j \in [1,n]\) ,\(y1\) 總是在最上面,也就是說下面的其他函數可以踢掉不要,但由於 \(sqrt\) 函數的增速遞減,會出現如圖中 \(y2,y4\) 的情況,即存在一個交點使得在該點兩邊時兩條直線的位置關系不同。此時如果沒有上面的 \(y1\),\(y2,y4\) 都有可能成為答案,所以不能亂踢。
看下面這種情況:
設 \(k_1\) 為 \(y_1,y_2\) 的交點,\(k_2\) 為 \(y_2,y_3\) 的交點。
此時 \(k_1 > k_2\),可以發現 \(y_2\) 始終在其他直線的下面,可以放心的將其踢掉。
所以維護出來的決策集合大概就是醬紫的:
對於不同的 \(i\) 來說,都有一個互不不同的直線在最上方,所以該決策集合里的直線都是有用的。可以從圖中看出,最優決策點單調遞增(決策單調性的數學證明較麻煩,本人能力不足,不作探討)。
維護決策集合用單調隊列,查找直線交點用二分,隨便搞搞就行了。
時間復雜度為 \(O(nlogn)\)。
【分治】
為方便描述,用 \(dp[a,b]\) 表示 \(dp[a],dp[a+1],dp[a+2]...dp[b]\)。
假設我們已知 \(dp[l,r]\) 的最優決策點均位於 \([L,R]\),再設 \(dp[mid]\) 的最優決策點為 \(j_0\),其中 \(mid=(l+r)/2\)。根據決策單調性的定義可知:
\(dp[l,mid-1]\) 的最優決策點位於 \([L,k]\),
\(dp[mid+1,r]\) 的最優決策點位於 \([k,R]\)。
原問題變成了兩個規模更小的同類型問題,所以可以用分治來對 \(dp\) 進行優化。
分治的話理解和代碼都要簡單一些,但在某些情況下可能要被卡,時間復雜度會嚴重退化,所以還是二分棧的實用性更高。
還是以 \(Lightning\) \(Conductor\) \([P3515]\) 為例,每次的分治中先暴力掃一遍找到 \(p[mid=(l+r)/2]\) 的最優決策點 \(j_0\),然后做一下左邊,再做一下右邊,然后 \(...\) 然后 \(...\) 就沒了。
時間復雜度為 \(O(nlogn)\)。
5.【Code】
二分棧
#include<algorithm>
#include<cstdio>
#include<cmath>
#define Re register int
using namespace std;
const int N=5e5+3;
int i,j,n,h,t,a[N],Q[N],Poi[N];
double p[N],sqr[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;
}
inline double Y(Re i,Re j){return a[j]+sqr[i-j];}
inline int find_Poi(int j1,int j2){//找到兩個直線的交點i
Re l=j2,r=n,mid,ans=n+1;//為了處理兩個直線沒有交點的情況,用一個變量記錄答案
while(l<=r){
mid=l+r>>1;
if(Y(mid,j1)<=Y(mid,j2))ans=mid,r=mid-1;
//當前這個位置i使得直線j1的縱坐標小於直線j2的縱坐標,說明這個點i在交點的右方,所以右邊界要縮小
else l=mid+1;
}
return ans;
}
inline void sakura(){
h=1,t=0;
for(i=1;i<=n;++i){//由於i本身也是一個決策點,所以要先入隊再取答案擇優
while(h<t&&Poi[t-1]>=find_Poi(Q[t],i))--t;//如果出現了上述可踢的情況,出隊
Poi[t]=find_Poi(Q[t],i),Q[++t]=i;
while(h<t&&Poi[h]<=i)++h;
//找到第一個位置j使得直線Q[j]與直線Q[j+1]的交點大於i,
//那么直線Q[j]就是i前面在最上面的直線,即答案,自己畫個圖模擬一下就懂了
p[i]=max(p[i],Y(i,Q[h]));
}
}
int main(){
in(n);
for(Re i=1;i<=n;++i)in(a[i]),sqr[i]=sqrt(i);
sakura();
for(Re i=1;i<n-i+1;++i)swap(a[i],a[n-i+1]),swap(p[i],p[n-i+1]);
sakura();
for(Re i=n;i;--i)printf("%d\n",(int)ceil(p[i])-a[i]);
}
分治
#include<algorithm>
#include<cstdio>
#include<cmath>
#define Re register int
using namespace std;
const int N=5e5+3;
int i,j,n,h,t,a[N],Q[N],Poi[N];
double tmp,p[N],sqr[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;
}
inline void sakura(Re l,Re r,Re L,Re R){
if(l>r)return;
Re mid=l+r>>1,j0;double mx=0;
for(Re j=L;j<=mid&&j<=R;++j)
//暴力找p[i]的最優決策點j0,而其決策點j必須滿足j<=i,即此處的j<=mid
if((tmp=a[j]+sqr[mid-j])>mx)mx=tmp,j0=j;
p[mid]=max(p[mid],mx);
sakura(l,mid-1,L,j0),sakura(mid+1,r,j0,R);
}
int main(){
in(n);
for(Re i=1;i<=n;++i)in(a[i]),sqr[i]=sqrt(i);
sakura(1,n,1,n);
for(Re i=1;i<n-i+1;++i)swap(a[i],a[n-i+1]),swap(p[i],p[n-i+1]);
sakura(1,n,1,n);
for(Re i=n;i;--i)printf("%d\n",(int)ceil(p[i])-a[i]);
}
6.【題目鏈接】
【中檔題】
-
\(Lightning\) \(Conductor\) \([P3515]\)
【標簽】動態規划/決策單調性 -
\(Noi\) 嘉年華 \([P1973]\)
【標簽】動態規划/決策單調性 -
大佬 \([P3724]\)
【標簽】動態規划/決策單調性
【高檔題】
- 詩人小 \(G\) \([P1912]\)
【標簽】動態規划/單調隊列/貪心
四:【單調隊列優化 DP】
【前言】
形如 \(dp[i]=max/min \{ dp[j]+funtion(i)+function(j) \}\) 的 \(dp\) 方程均可嘗試使用單調隊列優化。
單調棧和單調隊列給我們展現出了一種思想:在保證正確性的前提下,及時排除不可能的決策點,保持決策集合內部的有序性和查找決策的高效性。之前的二分棧,此處的單調隊列優化,和后面的斜率優化都是以此為核心來運作的。
其實質是優化 “決策”。
1.【單調隊列的簡單運用】
【T1】
琪露諾 \([P1725]\)(盜版滑動窗口QAQ)。
【題目大意】
在給定序列中找出一條路徑使其經過的點之和最大,且每次可走的距離在給定區間 \([l,r]\) 以內。
【分析】
方程非常簡單:\(dp[i]=max\{ dp[j]+a[i] \} (i-r \leqslant j \leqslant i-l)\) 。
在某一次決策中,由於決策點 \(j\) 只可能在 \([i-l,i-r]\) 這一段區間內,可以只將這些點放入決策集合。
而 \(l,r\) 是定值,當 \(i\) 不斷增大時,之前小於 \(i-l\) 的 \(j\) 現在還是小於 \(i-l\),所以可以永遠地踢掉。
若 \(j < j'\) 且 \(dp[j] \leqslant dp[j']\),那么 \(dp[j]\) 也可以永遠地踢掉。為什么呢? \(j\) 在 \(j'\) 的前面,一定會先成為不合法決策點,而 \(j\) 的價值又比 \(j'\) 小,因此留下來只是浪費掃描的時間。
最終維護出了一個價值遞減的單調隊列。
【Code】
#include<algorithm>
#include<cstring>
#include<cstdio>
#define Re register int
using std::max;
const int N=2e5+3;
int n,l,r,h=1,t,a[N],Q[N];
long long ans=-2e9,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;
}
int main(){
in(n),in(l),in(r);
memset(dp,-127,sizeof(dp));
Q[++t]=0;
for(Re i=0;i<=n;++i)in(a[i]);
dp[0]=a[0];
for(Re i=l;i<=n;i++){//注意枚舉起點是l不是1
while(h<=t&&dp[Q[t]]<=dp[i-l])--t;//維護單調遞減
Q[++t]=i-l;//入隊
while(h<=t&&Q[h]<i-r)++h;//保證決策點合法
dp[i]=dp[Q[h]]+a[i];//取出最優決策點
if(i>=n-r)ans=max(ans,dp[i]);
}
printf("%lld",ans);
}
【T2】
【題目大意】
\(M\) 個工人要對 \(N\) 塊木板進行粉刷。工人 \(i\) 要么不刷,要么就刷不超過 \(L_i\) 塊並且包含 \(S_i\) 的連續一段木板,每粉刷一塊木板有 \(P_i\) 的報酬。要求使總報酬最大。
【分析】
\(S_i\) 的散亂分布非常討厭,所以先把工人按 \(S_i\) 排個序。
主要信息有“工人序號”,“木板序號”,“報酬”這三個,而其中“報酬”為所求答案,可以用 \(dp[i][j]\) 表示前 \(i\) 個工人刷完前 \(j\) 塊木板所得總報酬。
考慮狀態轉移:
第 \(i\) 個工人可以跳過不刷木板,也可以跳過第 \(j\) 塊木板不刷,因此先在 \(dp[i-1][j]\) 和 \(dp[i][j-1]\) 當中取個最大值。
工人 \(i\) 想要粉刷的區間 \([k+1,j]\) 必須包括 \(S_i\),並且區間長度要小於等於 \(L_i\)。
所以得出 \(dp\) 轉移方程:\(dp[i][j]=max \{ dp[i-1][j],dp[i][j-1],dp[i-1][k]+P_i*(j-k) \}\),其中 \(S_i \leqslant j\) 且 \(k \in [j-L_i,S_i-1]\)。
\(k\) 為決策點,\(P_i*j\) 為定值可以單獨提出來,所以實際上就是上面琪露諾 \([P1725]\)一樣的類型,只是加了一維而已。
最終維護出了一個 \(dp[i-1][k]-P_i*k\) 遞減的單調隊列。
【Code】
#include<algorithm>
#include<cstring>
#include<cstdio>
#define Re register int
using namespace std;
const int N=16005;
struct QAQ{int L,P,S;}a[105];
int n,m,i,j,k,Q[N],W[N],dp[105][N];
inline bool cmp(QAQ a,QAQ b){return a.S<b.S;}
int main(){
while(scanf("%d%d",&n,&m)!=EOF){
memset(dp,0,sizeof(dp));
memset(a,0,sizeof(a));
for(i=1;i<=m;++i)scanf("%d%d%d",&a[i].L,&a[i].P,&a[i].S);
std::sort(a+1,a+m+1,cmp);
for(i=1;i<=m;++i){
Re l=1,r=0,tmp,Si=a[i].S,Li=a[i].L,Pi=a[i].P;
for(k=max(0,Si-Li);k<Si;++k){
//k+1為工人i開始刷的位置,max(1,Si-Li+1)<=k+1<=Si
tmp=dp[i-1][k]-Pi*k;
while(l<=r&&Q[r]<=tmp)--r;
Q[++r]=tmp,W[r]=k;
}
for(j=1;j<=n;++j){
dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
if(Si+Li>j&&j>=Si){//Si+Li-1>=j>=Si
while(l<=r&&W[l]+Li<j)++l;//W[r]+1+Li-1<j
if(l<=r)dp[i][j]=max(dp[i][j],Q[l]+Pi*j);
}
}
}
printf("%d\n",dp[m][n]);
}
}
2.【單調隊列優化多重背包】
先來回顧一下多重背包問題。
用 \(v,w,c\) 分別表示物品重量,價值,數量,\(n\) 為物品數量,\(V\) 為背包容量。\(dp\) 方程為:\(dp[j]=max\{ dp[j-k*v[i]]+k*w[i] \}\) \((j \in [v[i],V],\) \(k \in [1,min(c[i],j/v[i])])\)
還可以用二進制拆分來進行優化,但就是有這樣一道題,連 \(log\) 都要卡:多重背包 \([CodeVS5429]\)。所以還需要考慮更高效的算法。
但說來說去似乎都和單調隊列扯不上關系。
為何?
觀察 \(dp[j]\) 和 \(dp[j-1]\) 決策集合:
\(dp[j]: \{ j \% v[i]...j-2*v[i],j-v[i] \}\)
\(dp[j-1]: \{ (j-1) \% v[i]...(j-1)-2*v[i],(j-1)-v[i] \}\)
\(dp[j]\) 的每一個決策點都與 \(dp[j-1]\) 不同,很難根據 \(dp[j-1]\) 的決策集合轉移成 \(dp[j]\) 的決策集合。
再看 \(dp[j]\) 和 \(dp[j-v[i]]\):
\(dp[j]: \{ j-c[i]*v[i]...j-2v[i],j-v[i] \}\)
\(dp[j-v[i]]: \{ j-(c[i]+1)*v[i]...j-3v[i],j-2v[i] \}\)
可以發現上面只是比下面的區別僅僅在於 \(j-(c[i]+1)*v[i]\) 和 \(j-v[i]\) ,恰好滿足單調隊列的一個特性:但有新的決策出現時,決策點集合中會去掉一部分不合法的,再加上一部分新來的。
所以我們可以按照 \(j%v[i]\) 的余數來分一下:
\(dp[j](j\%v[i]=0):\{0,v[i],2v[i]...j-2v[i],j-v[i]\}\)
\(dp[j](j\%v[i]=1):\{1,v[i]+1,2v[i]+1...j-2v[i],j-v[i] \}\)
\(...\)
\(dp[j](j\%v[i]=v[i]-1):\{v[i]-1,2v[i]-1...j-2v[i],j-v[i]\}\)
設 \(j=p*v[i]+r\),那么原方程可改為: \(dp[p*v[i]+r]=max\{ dp[r+k*v[i]]+(p-k)*w[i] \}\) \((r \in [0,v[i]-1],\) \(p \in [1,\lfloor(V-r)/v[i]\rfloor],\) \(k \in [p-min( \lfloor V/w[i]\rfloor ,c[i]),p])\)
只要在最外層將 \(i,r,p\) 都枚舉出來,這就是一個標准的單調隊列可優化方程,用類似 \(fence\) \([POJ1821]\) 的方法維護即可。
時間復雜度為 \(O(N*V)\) 。
【Code】
#include<cstdio>
#define Re register int
const int N=7003,M=7003;
int n,h,t,V,mp,tmp,v[N],w[N],c[N],Q[N],K[N],dp[M];
inline void in(Re &x){
Re fu=0;x=0;char ch=getchar();
while(ch<'0'||ch>'9')fu|=ch=='-',ch=getchar();
while(ch>='0'&&ch<='9')x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
x=fu?-x:x;
}
inline int max(Re a,Re b){return a>b?a:b;}
inline int min(Re a,Re b){return a<b?a:b;}
int main(){
in(n),in(V);
for(Re i=1;i<=n;++i)in(v[i]),in(w[i]),in(c[i]);
for(Re i=1;i<=n;++i)
for(Re r=0;r<v[i];++r){
h=1,t=0,mp=(V-r)/v[i];
for(Re p=0;p<=mp;++p){
tmp=dp[p*v[i]+r]-w[i]*p;
while(h<=t&&Q[t]<=tmp)--t;
Q[++t]=tmp,K[t]=p;
while(h<=t&&p-K[h]>min(c[i],V/v[i]))++h;
dp[p*v[i]+r]=max(dp[p*v[i]+r],Q[h]+p*w[i]);
}
}
printf("%d",dp[V]);
}
3.【題目鏈接】
【簡單題】
- 琪露諾 \([P1725]\)
【標簽】動態規划/單調隊列
【中檔題】
-
\(fence\) \([POJ1821]\)
【標簽】動態規划/單調隊列 -
多重背包 \([CodeVS5429]\)
【標簽】動態規划/單調隊列/多重背包 -
我要長高 \([UESTC594]\)
【標簽】動態規划/單調隊列
五:【斜率優化 DP】
由於篇幅過大,已搬出。。。。。
補充:其實質是優化 “決策”。
【參考文獻】
(本文部分內容摘自以下文章)
-
\(dp\) 基礎 — \(DraZxINDdt\)
-
《算法競賽進階指南》李煜東
-
《動態規划算法的優化技巧》毛子青