1. 程式人生 > 其它 >13.經典動態規劃:0-1揹包的變體

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時,若xtrue,則說明可以恰好將揹包裝滿,若xfalse,則說明不能恰好將揹包裝滿。

比如說,如果dp[4][9] = true,其含義為:對於容量為 9 的揹包,若只是用前 4 個物品,可以有一種方法把揹包恰好裝滿。

或者說對於本題,含義是對於給定的集合中,若只對前 4 個數字進行選擇,存在一個子集的和可以恰好湊出 9。

根據這個定義,我們想求的最終答案就是dp[N][sum/2],base case 就是dp[..][0] = truedp[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];  
}