【演演算法】基礎DP

2022-10-20 18:01:16

參考資料

揹包九講

一、線性DP

  1. 如果現在在狀態 i 下,它上一步可能的狀態是什麼。
  2. 上一步不同的狀態依賴於什麼。

根據上面的分析,分析出狀態和轉移方程。注意:dp 不一定只有兩維或者一維,一開始設計狀態時先不考慮維度。如果空間超了的話考慮捲動陣列等優化,或者再重新設計狀態。

可以通過哪一個條件範圍小來入手設計狀態。

對於邊界條件:一般是考慮第一個點的特殊情況,即 \(dp_{[1]}\)

二、揹包問題

1. 01揹包

模型總結:每個物品只能選一次。

思想

每件物品可以通過放或者不放來轉移,所以它還依賴於之前使用的容量。設 \(dp_{[i][j]}\) 表示前 \(i\) 件物品用了 \(j\) 的容量的最大價值。不難得出:\(dp_{[i][j]}=max(dp_{[i-1][j]},dp_{[i-1][j-v[i]]+w_[i]})\)

考慮優化維度,常用的方法是優化掉前 \(i\) 個物品的維度(對很多題目也同樣適用)。設 \(dp_{[j]}\) 表示目前位置使用 \(j\) 的容量的最大價值。於是狀態轉移方程變成了:\(dp_{[j]}=max(dp_{[j]},dp_{[j-v[i]]}+w_{[i]})\)

由於 \(v_{[i]}\ge 0\),所以 \(j-v_{[i]}\le j\),為了保證 \(j-v_{[i]}\) 一定是上一次 \(i-1\) 個物品的,所以我們迴圈 \(j\) 的時候要倒序迴圈。或者畫出二維 DP 轉移列表來理解。

程式碼:

for(int i=1;i<=n;i++)
	for(int j=m;j>=v[i];j--)
		dp[j]=max(dp[j],dp[j-v[i]]+w[i]);

2. 完全揹包

模型總結:每個物品可以選無限次

思想

按照 01 揹包的思想設計出初始狀態:設 \(dp_{[i][j]}\) 表示前 \(i\) 件物品用了 \(j\) 的容量的最大價值。然後迴圈列舉選 \(k\) 個物品,仿照上面轉移方程求出最大值。

繼續考慮優化,依舊設 \(dp_{[j]}\) 表示目前位置使用 \(j\) 的容量的最大價值。但這次不能再回圈 \(k\) 個物品了。我們返回去思考為什麼 01 揹包迴圈 \(j\) 要倒序迴圈,因為 01 揹包只能選 1 個。那麼如果它正序迴圈的話,\(dp_{[j]}\) 還有可能取到現在正在取的 \(i\) 個物品(因為 \(dp_{j-v[i]}\) 可能在這次迴圈中已經被更新過了),跟完全揹包的模型相符。

程式碼

for(int i=1;i<=n;i++)
	for(int j=v[i];j<=m;j++)
		dp[j]=max(dp[j],dp[j-v[i]]+w[i]);

3. 多重揹包

模型總結:每個物品有各自可選的最多次數 \(k\)

思想

按照完全揹包暴力的思想同樣可以得到暴力解法。

時間複雜度超時的原因主要是由於迴圈了 \(k\) 這個維度,於是接下來需要優化這個維度。這裡可以使用二進位制優化,將回圈列舉的 \(k\)\([1,p]\)\(p\) 是最多選的個數)變為 \(1,2,4...2^{k-1},p-2^k+1\)\(k\) 是滿足 \(p-2^k+1>0\) 的最大整數)。

現在證明 \([1,p]\) 的所有數都可以被寫成上面轉化後的數的和:

對於 \(S_1=[0,(2^k-1)]\) 這一區間的數:\(2^x,2^{x-1}\) 中間相差了一個 \(2^{x-1}\),如果 \([1,2^{x-1}-1]\) 中的所有數都可以被寫成上面的形式,那麼 \([2^{x-1},2^x]\) 中的所有數也可以滿足條件,即 \([1,2^x]\) 都會滿足條件。於是 \(x\) 的值追根溯源到 \(0\),此時顯然 \([1,1]\) 中的所有數都會滿足條件,再根據上面的遞推就可以證明出來了。

對於 \(S_2=[2^k,p]\) 這一區間的數:\(p=(p-2^k+1)+2^k-1\)。由於這一區間內都是連續的,所以所有數都可以被表示為 \((p-2^k+1)+x\;(x\in S1)\)。證明完畢。

程式碼

for(int i=1;i<=n;i++){
	int maxi;
	if(v[i]==0) maxi=p[i];
	else maxi=min(p[i],t/v[i]);
	for(int k=1;maxi>0;k<<=1){
		k=min(k,maxi);
		maxi-=k;
		for(int j=t;j>=v[i]*k;j--) dp[j]=max(dp[j],dp[j-v[i]*k]+w[i]*k);
	}
}

4. 其他

混合揹包:只需分類討論上述情況即可。

二維揹包費用(選一個物品有兩個價值):只需再多增一維按照上面來轉移即可。有時另一種價值的表述方法也會變成「最多選 \(x\) 件物品」。

分組揹包(每組裡面最多選一件物品):對每組的 \(dp_{[j]}\) 取最大即可。

依賴揹包(有些物品必需先選另一種物品後才能選):對於每一個沒有依賴的揹包分類討論,只選它自己,選它自己+依賴1……等。

三、區間DP

對於有些一眼能看出來的區間DP題一般都是拆分類/合併類的題,典型的有:P1880 合併石子。但關鍵要看大區間內的答案能否可以通過小區間內的答案轉移而來。

將在一段大區間內進行的DP,分解成小區間內進行的DP,大都需要以下幾個條件:

  1. 跨度(階段)
  2. 起點(狀態)
  3. 合併的位置(決策的位置)

比較典型的虛擬碼:
核心思路:先計算出小區間,再推出大區間,邊界一般為區間長為 1 的時候

for 子區間長度 2 to n;
   for 起點 1 to 終點不大於n
       終點=起點+子區間長度-1
       for 決策點 起點 to 終點-1
          狀態轉移方程

四、狀壓DP

對於有多種情況可以影響答案的時候,通常選擇暴力開維度來儲存資訊。但有時會導致維度太多而爆空間,這時候我們要將形式相同的維度壓縮成一維,這就是狀壓DP

狀壓DP的一大重點便是位運算,而位運算又會引申出位元運算。下面是幾種常見位運算及位元運算的意義

一個二進位制數位 &1 得到它本身。(& 表示兩個數的相同位上,只有兩個都為 \(1\),最終的值才為 \(1\)。通常被看做兩個集合的交集。)

一個二進位制數位 ^1 則取反。(^ 表示兩個數的相同位上,只有值不同,最終的值才為 \(1\)

一個二進位制數位 &0 則賦值為 \(0\)

一個二進位制數位 |1 則賦值為 \(1\)。(| 表示兩個數的相同位上,只要有一個為 \(1\),最終值就為 \(1\)

(n>>k)&1 取出二進位制下 \(n\) 的第 \(k\) 位 (從右往左)。

n&((1<<k)-1) 取出二進位制下 \(n\) 的右 \(k\) 位。

n^(1<<k) 將二進位制下 \(n\) 的第 \(k\) 位取反。

n|(1<<k) 將二進位制下 \(n\) 的第 \(k\) 位賦值為 \(1\)

n&(-(1<<k)) 將二進位制下 \(n\) 的第 \(k\) 位賦值為 \(0\)