【學習筆記】動態規划—各種 DP 優化


【學習筆記】動態規划—各種 DP 優化

【大前言】

個人認為貪心,\(dp\) 是最難的,每次遇到題完全不知道該怎么辦,看了題解后又瞬間恍然大悟(TAT)。這篇文章也是花了我差不多一個月時間才全部完成。


【進入正題】

用動態規划解決問題具有空間耗費大時間效率高的特點,但也會有時間效率不能滿足要求的時候,如果算法有可以優化的余地,就可以考慮時間效率的優化。


【DP 時間復雜度的分析】

\(DP\) 高時間效率的關鍵在於它減少了“冗余”,即不必要的計算或重復計算部分,算法的冗余程度是決定算法效率的關鍵。而動態規划就是在將問題規模不斷縮小的同時,記錄已經求解過的子問題的解,充分利用求解結果,避免了反復求解同一子問題的現象,從而減少“冗余”。
但是,一個動態規划問題很難做到完全消除“冗余”。

下面給出動態規划時間復雜度的決定因素:

時間復雜度 \(=\) 狀態總數 \(×\) 每個狀態轉移的狀態數 \(×\) 每次狀態轉移的時間


【DP 優化思路】

一:減少狀態總數

\((1).\) 改進狀態表示

\((2).\)選擇適當的規划方向

二:減少每個狀態轉移的狀態數

\((1).\) 四邊形不等式和決策的單調性

\((2).\) 決策量的優化

\((3).\) 合理組織狀態

\((4).\) 細化狀態轉移

三:減少狀態轉移的時間

\((1).\) 減少決策時間

\((2).\) 減少計算遞推式的時間

(上述內容摘自 《動態規划算法的優化技巧》毛子青 ,想要深入了解其思想的可以去看看這篇寫得超級好的論文。)


看到這里是不是已經感覺有點蒙了呢?
本蒟蒻總結了一個簡化版本:


【DP 三要點】

在推導 \(dp\) 方程時,我們時常會感到毫無頭緒,而實際上 \(dp\) 方程也是有跡可循的,總的來說,需要關注兩個要點:狀態決策轉移。其中 “狀態” 又最為關鍵,決策最為復雜。

【狀態】

關於 “狀態” 的優化可以從很多角度出發,思維難度及其高,有時候狀態選擇的好壞會直接導致出現暴零和滿分的分化。

【決策】

“狀態” 不同,“決策” 優化則有着大量模板化的東西,在各大書籍,文章上你都可以看到這樣的話:只要是形如 \(XXX\) 的狀態轉移方程,都可以用 \(XXX\) 進行優化。

【轉移】

“轉移” 則指由最優決策點得到答案的轉移過程,其復雜度一般較低,通常可以忽略,但有時也需要特別注意並作優化。

本文將會重點針對 “決策” 優化部分作一些總結,記錄自己的感悟和理解。


\[QAQ \]


一:【矩陣優化 DP】

\(updata\) 之后由於篇幅過大,已搬出。。。。。

【學習筆記】動態規划—矩陣遞推加速

補充:其實質是優化 “轉移”


\[QAQ \]


二:【數據結構優化 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.【題目鏈接】

【簡單題】

【高檔題】


\[QAQ \]


三:【決策單調性】

【前言】

形如 \(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.【題目鏈接】

【中檔題】

【高檔題】


\[QAQ \]


四:【單調隊列優化 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】

\(fence\) \([POJ1821]\)

【題目大意】

\(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.【題目鏈接】

【簡單題】

【中檔題】


\[QAQ \]


五:【斜率優化 DP】

由於篇幅過大,已搬出。。。。。

【學習筆記】動態規划—斜率優化DP(超詳細)

補充:其實質是優化 “決策”


\[QAQ \]


【參考文獻】

(本文部分內容摘自以下文章)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM