13.經典動態規劃:0-1揹包的變體
子集切分問題(LeetCode 416 難度:中等)
描述
對於這個問題,看起來和揹包沒有任何關係,為什麼說它是揹包問題呢?
首先回憶一下揹包問題大致的描述是什麼:
給你一個可裝載重量為W
的揹包和N
個物品,每個物品有重量和價值兩個屬性。其中第i
個物品的重量為wt[i]
,價值為val[i]
,現在讓你用這個揹包裝物品,最多能裝的價值是多少?
那麼對於這個問題,我們可以先對集合求和,得出sum
,把問題轉化為揹包問題:
給一個可裝載重量為sum/2
的揹包和N
個物品,每個物品的重量為nums[i]
。現在讓你裝物品,是否存在一種裝法,能夠恰好將揹包裝滿?
你看,這就是揹包問題的模型,甚至比我們之前的經典揹包問題還要簡單一些,下面我們就直接轉換成揹包問題
解法分析
第一步要明確兩點,「狀態」和「選擇」。
狀態就是「揹包的容量」
和「可選擇的物品」
,選擇就是「裝進揹包」
或者「不裝進揹包」
。
第二步要明確dp
陣列的定義。
按照揹包問題的套路,可以給出如下定義:
dp[i][j] = x
表示,對於前i
個物品,當前揹包的容量為j
時,若x
為true
,則說明可以恰好將揹包裝滿,若x
為false
,則說明不能恰好將揹包裝滿。
比如說,如果dp[4][9] = true
,其含義為:對於容量為 9 的揹包,若只是用前 4 個物品,可以有一種方法把揹包恰好裝滿。
或者說對於本題,含義是對於給定的集合中,若只對前 4 個數字進行選擇,存在一個子集的和可以恰好湊出 9。
根據這個定義,我們想求的最終答案就是dp[N][sum/2]
,base case 就是dp[..][0] = true
和dp[0][..] = false
,因為揹包沒有空間的時候,就相當於裝滿了,而當沒有物品可選擇的時候,肯定沒辦法裝滿揹包。
第三步,根據「選擇」,思考狀態轉移的邏輯。
回想剛才的dp
陣列含義,可以根據「選擇」對dp[i][j]
得到以下狀態轉移:
如果不把nums[i]
算入子集,或者說你不把這第i
個物品裝入揹包,那麼是否能夠恰好裝滿揹包,取決於上一個狀態dp[i-1][j]
,繼承之前的結果。
如果把nums[i]
算入子集,或者說你把這第i
個物品裝入了揹包,那麼是否能夠恰好裝滿揹包,取決於狀態dp[i - 1][j-nums[i-1]]
首先,由於i
是從 1 開始的,而陣列索引是從 0 開始的,所以第i
個物品的重量應該是nums[i-1]
,這一點不要搞混。
dp[i - 1][j-nums[i-1]]
也很好理解:你如果裝了第i
個物品,就要看揹包的剩餘重量j - nums[i-1]
限制下是否能夠被恰好裝滿。
換句話說,如果j - nums[i-1]
的重量可以被恰好裝滿,那麼只要把第i
個物品裝進去,也可恰好裝滿j
的重量;否則的話,重量j
肯定是裝不滿的。
最後一步,把偽碼翻譯成程式碼,處理一些邊界情況。
以下是我的 C++ 程式碼,完全翻譯了之前的思路,並處理了一些邊界情況:
boolcanPartition(vector<int>&nums) {
int sum= 0;
for (int num:nums)sum+=num;
//和為奇數時,不可能劃分成兩個和相等的集合
if (sum% 2 != 0) return false;
int n=nums.size();
sum=sum/ 2;
vector<vector<bool>>
dp(n+ 1, vector<bool>(sum+ 1, false));
//basecase
for (int i= 0;i<=n;i++)
dp[i][0]= true;
for (int i= 1;i<=n;i++){
for (int j= 1;j<=sum;j++){
if (j-nums[i- 1]< 0){
//揹包容量不足,不能裝入第i個物品
dp[i][j]=dp[i- 1][j];
} else {
//裝入或不裝入揹包
dp[i][j]=dp[i- 1][j]|dp[i- 1][j-nums[i-1]];
}
}
}
return dp[n][sum];
}