【LeetCode動態規劃#06】分割等和子集(01揹包問題一維寫法實戰)

dayceng發表於2023-04-09

分割等和子集

分割等和子集

給你一個 只包含正整數 的 非空 陣列 nums 。請你判斷是否可以將這個陣列分割成兩個子集,使得兩個子集的元素和相等。

示例 1:

輸入:nums = [1,5,11,5]
輸出:true
解釋:陣列可以分割成 [1, 5, 5] 和 [11]

示例 2:

輸入:nums = [1,2,3,5]
輸出:false
解釋:陣列不能分割成兩個元素和相等的子集。

提示:

1 <= nums.length <= 200
1 <= nums[i] <= 100

思路

題意分析

題目是要找是否可以將這個陣列分割成兩個子集,使得兩個子集的元素和相等。

那麼只要找到集合裡能夠出現 sum / 2 的子集總和,就算是可以分割成兩個相同元素和子集了。

例如示例1:

輸入元素總和是22,要分成兩個元素和相等的子集,那這兩個子集各自之和肯定都是11,即sum / 2

解題方法

既然這題出現在dp章節了,那肯定就是用dp來做了

但是,如果第一次遇見,應該怎麼確定適用dp來解呢?可以先嚐試分析一下題目,看看能不能把題目往這邊靠

比如,這題中,每個構成子集的每個元素只能使用一次(關鍵點1)

這個描述可以聯想到01揹包問題,那就嘗試去抽象一下:

本題可以轉換為一個01揹包問題,目標是給定一個陣列,用陣列中的元素去填滿一個容量為 sum / 2 的揹包(如果能填滿的話,就找到了題目要求的子集)

還不夠,還要繼續確定什麼是物品(物品重量),什麼是價值

顯然,能夠作為物品的只有陣列裡的元素了,那麼物品的重量就是元素數值,物品的價值也是元素數值

現在就把01揹包問題套進來了,當前問題的條件如下:

  • 揹包的體積是 sum / 2
  • 要放入揹包的物品是陣列元素,其重量為元素數值,重量也為元素數值
  • 揹包中每個元素不可重複使用
  • 如果揹包正好裝滿,那麼說明找到了總和為 sum / 2 的子集(有一種就行,不用最優)

ok,現在可以五部曲去分析了

五步走

以一維01揹包問題為模板來解題,詳見

1、確定dp陣列含義

回顧一下分析一維陣列的揹包問題時,dp[j]的定義:在容量為j時,揹包能裝下的物品的最大價值

根據上面的分析,本題中物品重量是陣列元素數值,物品價值還是陣列元素數值

那上面的定義可以改為:揹包總容量是j,放入物品後,揹包的最大重量為dp[j]

即本題dp陣列的含義

舉個例子,假設揹包容量為target,那麼dp[target]就是揹包裝滿時的重量

當dp[target] = target,揹包就裝滿了,此時變找到了題解子集

這裡會出現一種情況:在所給的揹包容量下,沒有物品能裝滿

什麼意思?舉個例子,輸入是陣列 [1, 5, 11, 5],target是7

那麼,當dp[7]的時候,揹包只能放1、5,總價為6,沒放滿,顯然這不是我們需要的結果(不滿足dp陣列定義),不過好在我們是倒序遍歷,在之後的遍歷中,dp[6] = 6,滿足dp定義,並且我們也找到了一個滿足條件的子集[1,5]

2、確定遞推公式

其實可以直接套用一維dp陣列揹包問題的遞推公式:dp[j] = max(dp[j], dp[j - wight[i]] + value[i])

本題中,物品重量和價值都是陣列元素,所以可以改成:dp[j] = max(dp[j], dp[j - nums[i]] + nums[i])

其含義是:

不放入物品時,容量為j的揹包最大的價值是dp[j];

放入物品時,容量為j - nums[i]的揹包所背的最大價值是dp[j - nums[i]];

3、確定如何初始化

還是參考一維揹包問題,當揹包容量為0,也就是dp[0]時,裡面不可能裝東西

因此,dp[0] = 0;

其他部分全部初始化為0(因為題目說了陣列元素全是正數),目的是為了防止倒序遍歷過程中,上一層的值與當前值疊加(詳見:一維dp陣列如何初始化

又因為題目給了:每個陣列元素不會超過100,陣列本身的大小不會超過200,且元素總和不會超過20000

那麼建立dp陣列時只需要取一半的大小即可

vector<int> dp(10001, 0);
4、確定遍歷順序

都套一維dp陣列的揹包問題了,肯定是倒序遍歷

原因詳見:為什麼要倒序遍歷?

for(int i = 0; i < nums.size(); ++i){//遍歷物品
    for(int j = target; j > 0; --j){//遍歷揹包容量
        dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
    }
}

程式碼

根據上面的分析可寫出一下程式碼

步驟:

1、求陣列的總和,用於求揹包容量target

2、判斷當前總和sum是否為偶數,不為偶數就直接返回false

3、根據題目提示定義dp陣列,同時進行初始化

4、倒序遍歷dp陣列,先遍歷物品,再遍歷揹包容量,計算dp[j]

5、判斷dp[target] 是否等於 target

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        //求揹包容量,也就是target
        //先定義一個sum用於求陣列總和
        int sum = 0;
        for(auto num : nums) sum += num;

        //如果sum值不能均分(不是偶數),那麼一定無法分為兩個和相等的子集,直接返回false
        if(sum % 2 == 1) return false;
        int target = sum / 2;

        //定義dp陣列,同時初始化
        vector<int> dp(10001, 0);
        // //初始化dp陣列
        // dp[0] = 0;
        //遍歷dp陣列(注意是倒序遍歷)
        for(int i = 0; i < nums.size(); ++i){//遍歷物品
            // 如果當前揹包容量小於物品重量,換一個物品繼續遍歷容量
            // 每一個元素一定是不可重複放入,所以從大到小遍歷
            for(int j = target; j >= nums[i]; --j){//遍歷揹包容量
                dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
            }
        }
        //判斷是否滿足題意條件
        if(dp[target] == target) return true;
        return false;
    }
};

相關文章