標籤:演算法 動態規劃dp 完全背包與多重背包問題 計數原理
上一節,我們討論了01背包問題,說明了*
遞迴與分治法 與 動態規劃DP的區別和聯絡,介紹了緩衝的概念*。以下,我們用DC、DP、cache分別表示分治法、動態規劃和緩衝。本節,我們討論01背包的另外兩種形似——
完全背包和多重背包問題,分析DP問題的另外一些情況。
例一:完全背包問題
同樣有n種價值和重量分別為weight[i] and value[i], 背包大小W。限制條件:每種物品數目是無限的。問:能挑選出來的總價值最大的物品組合總重量?
分析一:這裡,我們無視數量有限這個條件,可以得到下列遞迴式:
dp[i+1][j] = max{dp[i][j-k*weight[i]] + k*value[i] | k>=0且k*weight[i]<=j}
下面,來實現這個動態規劃(留給讀者)。很不幸,我們發現這次的時間複雜度變成了三重迴圈,O(nW^2). 因為,此時將子問題組成原問題,需要的組合數目是W個而不是常數項。
分析二:
對上述遞迴式進行變形,可以變成dp[i+1][j] = max{dp[i][j], dp[i+1][j-w[i]] + v[i]};這樣就可以將時間複雜度從O(nW^2)轉化成O(nW).這個變形可以完全用數學化公式來實現,對應的物理意義:從前i+1號物品中選取j重的組合包含兩種:1)不包含i+1號物品dp[i][j] 2) 包含至少一個i+1號物品dp[i+1][j-w[i]] + v[i].
最佳化2:DP數組的重複利用
另外,我們注意到,在在遞推關係dp[i+1][j] = max{dp[i][j], dp[i+1][j-w[i]] + v[i]}中,dp[i+1][j]中,計算d[i+1][j]的時候僅僅用到了上一行左側的資料,所有我們可以對數組進行重複利用,這樣能夠減少空間上的時間複雜度和程式執行時間(程式具有更好的局部性):
dp[j] = max(dp[j], dp[j-w[i]] + v[i]);
例二:01背包問題之二和第一節中同樣的背包問題,我們增加如下的限制條件:
1< n< 100
1< wi < 10^7
1< vi < 100
1< W < 10^9
此時,顯然O(nW)的演算法時間複雜度就不是能夠接受的了;此時我們需要對子問題的定義作出變形—— dp[i][j]原來表示前i號物品選取重量不超過j的組合能產生的最大值, 現在dp[i][j]:前i號物選取價值為j的組合佔用的最小重量,最後取出dp[n][j]<=W 的最大的j即可。
此時:初始化dp[0][0]=0;dp[0][j]=INF
dp[i+1][j] = min(dp[i][j], dp[i][j - v[i]] + w[i]);
例子三:多重部分和問題有n種大小不同的數字ai,每種mi個,是否可以從中選取組合,使得它們的和是K
限制條件:
n<100;ai, mi<10^5; K<10^5
分析一:套用完全背包問題
dp[i][j]:前i號數字是否能組成和為j的組合;這樣得到遞推關係dp[i+1][j] ||= dp[i][j-k*a[i]];
時間複雜度O(nKK);這個時間複雜度並不好;一般來說DP的子問題不應該用來儲存bool的結果,這意味著我們損失了更多的資訊。
我們仔細來分析一下時間複雜度主要耗費在什麼地方,先看沒有最佳化之前的代碼
for (i = 0; i < n; ++i){
for (j = 0; j <=K; ++j){
for (k = 0; k < m[i] && k*a[i] <=j ; ++k){
dp[i+1][j] |= dp[i][j-k*a[i]];
}
}
}
分析2:“ 最佳化成最優子結構”
顯然,for k 部分的迴圈,直觀感覺是有冗餘。分類一下,如果能從前i中數字選擇出K和,a[i]可能被選取1次以上或者沒有被選取
如果a[i]沒有被選取,那麼意味著dp[i][j]=true;
如果a[i]被選取了1次以上,意味著dp[i+1][j-a[i]]=true;
也就是說,dp[i+1][j]本來是和dp[i][j-k*a[i]]這麼多子問題相關的;但是現在被轉化成兩個子問題dp[i][j] 和dp[i+1][j-a[i]],是不是很神奇?很顯然,在這裡,我們通過分治法對子問題進行了合并。
進一步,如果定義dp[i+1][j]:前i種數加和得到j的時候,a[i]剩餘的個數,則有
dp[i+1][j]={
m[i]; if(dp[i][j]>=0)
dp[i+1][j-a[i]] -1;(j-a[i]>=0 && dp[i+1][j-a[i]]>=0)
-1;(otherwise)
}
總結:在計數原理中,這種 子問題分類合并的思想和方法非常常用!
動態規劃第二講——完全背包與多重背包問題