1. 程式人生 > 其它 >單調佇列優化DP

單調佇列優化DP

主要內容

形如這樣\(\operatorname{DP}\) 轉移方程:

\[dp[i]=\max_{L_i\le j\le R_i}{\{dp[i]+val(i,j)\}} \]

滿足:

  1. \(\{L_i\}\) , \(\{R_i\}\) 遞增( 前提條件 )。

  2. \(R_i \le i\) ( 轉移條件 )。

  3. \(val(i,j)\) 值只與 \(j\) 相關 ( 根本優化轉移前提 ) 。

維護一個滑動視窗,每次求視窗中的最大值。對於兩個點 \(x\)\(y\) ,如果 \(x < y\)\(f(x) < f(y)\) ,那麼 \(y\) 進入視窗後,決策點一定不會是 \(x\)

用一個單調佇列維護窗口裡所有可能用到的決策點。

視窗右端點向右滑動時,把一個新的點插入隊尾。隊尾點為 \(q[r]\) ,新點為 \(x\) ,如果 \(f(q[r]) \le f(x)\) ,那麼 \(q[r]\) 沒用,把 \(q[r]\) 彈掉。重複過程直到隊尾點可能有用,即 \(f(q[r]) > f(x)\) ,把 \(x\) 入隊。

佇列中的 \(f(i)\) 從隊首到隊尾遞減。決策時,首先彈掉隊首超過範圍的點。這時隊首點就是決策點。\(f(i) = \max {\{f(j) + w_i\}} [i-R_i \le j \le i-L_i]\)

單調佇列優化 也稱為 滑動視窗


變式 \(-\) 單調佇列優化多重揹包

內容

\(dp[i][j]\) 表示前 \(i\) 個物品放入容量為 \(j\) 的揹包的最大收益 。

\[dp[i][j]=\max_{k=0}^{k\le k[i]}{\{dp[i-1][j-k\times c[i]]+k\times w[i]\}} \]

考慮 \(dp\) 的轉移 。

\[0\le p < c[i],0\le j \le \left\lfloor \dfrac{V-p}{c[i]}\right\rfloor,0\le k \le k[i] \]\[dp[i][p+j\times c]=\max{\{dp[i-1][p+(j-k)\times c]+k\times w\}} \]\[dp[i][p+j\times c]=\max{\{dp[i-1][p+(j-k)\times c]-(j-k)\times w+j\times w\}} \]\[dp[i][p+j\times c]=\max{\{dp[i-1][p+(j-k)\times c]-(j-k)\times w\}}+j\times w \]

這樣就可以進行單調佇列優化了 。

時間複雜度:\(O(nV)\)

核心程式碼:( P1776 寶物篩選 ) 程式碼中的 \(pos\) 就是上面的 \(j-k\)

struct Data{ int pos,val; }q[Maxv];

n=rd(),V=rd();
for(int i=1;i<=n;i++)
{
	 w=rd(),c=rd(),sum=rd();
	 if(!c) { ans+=w*sum; continue; }
	 sum=min(V/c,sum);
	 for(int p=0;p<c;p++)
	 {
	 	 s=(V-p)/c,l=1,r=0;
	 	 for(int j=0;j<=s;j++)
	 	 {
	 	 	 while(l<=r && q[r].val<=dp[p+j*c]-j*w) r--;
	 	 	 q[++r]=(Data){j,dp[p+j*c]-j*w};
	 	 	 while(l<=r && j-q[l].pos>sum) l++; // k>sum 時不合法 
	 	 	 dp[p+j*c]=max(dp[p+j*c],q[l].val+j*w);
		 }
	 }
}
printf("%d\n",ans+dp[V]);

多重揹包的其他解法:二進位制分組優化 ,時間複雜度: \(O(V\sum_{i=1}^{n}\log_2{k_i})\) ,見揹包問題

注意:

用結構體儲存單調佇列,防止反覆修改 \(dp\) 值。

並注意 \(j=0\) 時的情況,及時更新。


例題

P1725 琪露諾

狀態:設 \(dp[i]\) 表示走到 \(i\) 的最大收益。

\(L\)\(R\) 都是上文中的轉移範圍。

核心程式碼:

n=rd(),tmpl=rd(),tmpr=rd();
for(int i=0;i<=n;i++) a[i]=rd();
for(int i=1;i<=n;i++) L[i]=i-tmpr,R[i]=i-tmpl;
memset(dp,-inf,sizeof(dp));
dp[0]=a[0];
for(int i=1;i<=n;i++) // 必須從 l 開始 
{
	 if(R[i]<0) continue;
	 while(l<=r && q[l]<L[i]) l++;
	 while(l<=r && dp[q[r]]<=dp[R[i]]) r--;
	 q[++r]=R[i]; // 因為 i-L 小於 i ,所以應該確保最有決策再進行轉移 
	 dp[i]=dp[q[l]]+a[i];
}
int ans=-inf;
for(int i=L[n]+1;i<=n;i++) ans=max(ans,dp[i]);
printf("%d\n",ans);

P3572 [POI2014]PTA-Little Bird

狀態:\(dp[i]\) 表示到 \(i\) 為止的最小代價。

核心程式碼:

bool Better(int x,int y)
{
	 if((dp[x]<dp[y]) || (dp[x]==dp[y] && h[x]>=h[y])) return true;
	 return false;
}
for(int i=1;i<=n;i++) L[i]=i-k,R[i]=i-1;
q[1]=l=r=1;
for(int i=2;i<=n;i++)
{
	 if(R[i]<0) continue;
	 while(l<=r && q[l]<L[i]) l++;
	 while(l<=r && Better(R[i],q[r])) r--;
	 q[++r]=R[i];
	 dp[i]=dp[q[l]]+(h[q[l]]<=h[i]);
}
printf("%d\n",dp[n]);

P3957 跳房子

\(\rightarrow\) P3957 solution

P1099 樹網的核 \(\&\) P2491 [SDOI2011]消防(加強版 樹網的核)

(多倍經驗)

\(\rightarrow\) P1099 solution

CF372C Watching Fireworks is Fun

\(\rightarrow\) CF372C solution

燒橋計劃

\(\rightarrow\) 燒橋計劃 solution

P2254 [NOI2005]瑰麗華爾茲

\(\rightarrow\) P2254 solution

P2569 [SCOI2010]股票交易

\(\rightarrow\) P2569 solution