416 分割等和子集

2021-10-24 19:38:14 字數 4170 閱讀 3288

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

注意:每個陣列中的元素不會超過 100

陣列的大小不會超過 200

示例 1:

輸入: [1, 5, 11, 5]

輸出: true

解釋: 陣列可以分割成 [1, 5, 5] 和 [11].

示例 2:

輸入: [1, 2, 3, 5]

輸出: false

解釋: 陣列不能分割成兩個元素和相等的子集.

分析:(動態規劃)

可以轉化成0-1揹包問題

做這道題需要做乙個等價轉換:是否可以從輸入陣列中挑選出一些正整數,使得這些數的和 等於 整個陣列元素的和的一半。很坦白地說,如果不是老師告訴我可以這樣想,我很難想出來。

容易知道:陣列的和一定得是偶數。

本題與 0-1 揹包問題有乙個很大的不同,即:

0-1 揹包問題選取的物品的容積總量 不能超過 規定的總量;

本題選取的數字之和需要 恰好等於 規定的和的一半。

這一點區別,決定了在初始化的時候,所有的值應該初始化為 false。

「0 – 1」 揹包問題的思路

作為「0-1 揹包問題」,它的特點是:「每個數只能用一次」。解決的基本思路是:物品乙個乙個選,容量也一點一點增加去考慮,這一點是「動態規劃」的思想,特別重要。

在實際生活中,我們也是這樣做的,乙個乙個地嘗試把候選物品放入「揹包」,通過比較得出乙個物品要不要拿走。

具體做法是:畫乙個 len 行,target + 1 列的**。這裡 len 是物品的個數,target 是揹包的容量。len 行表示乙個乙個物品考慮,target + 1多出來的那 1 列,表示揹包容量從 0 開始考慮。很多時候,我們需要考慮這個容量為 0 的數值。

狀態與狀態轉移方程

狀態定義:dp[i][j]表示從陣列的 [0, i] 這個子區間內挑選一些正整數,每個數只能用一次,使得這些數的和恰好等於 j。

狀態轉移方程:很多時候,狀態轉移方程思考的角度是「分類討論」,對於「0-1 揹包問題」而言就是「當前考慮到的數字選與不選」。

不選擇 nums[i],如果在 [0, i – 1] 這個子區間內已經有一部分元素,使得它們的和為 j ,那麼 dp[i][j] = true;

選擇 nums[i],如果在 [0, i – 1] 這個子區間內就得找到一部分元素,使得它們的和為 j – nums[i]。

狀態轉移方程:

dp[i][j] = dp[i - 1][j] or dp[i - 1][j - nums[i]]
一般寫出狀態轉移方程以後,就需要考慮初始化條件。

j – nums[i] 作為陣列的下標,一定得保證大於等於 0 ,因此 nums[i] <= j;

注意到一種非常特殊的情況:j 恰好等於 nums[i],即單獨 nums[j] 這個數恰好等於此時「揹包的容積」 j,這也是符合題意的。

因此完整的狀態轉移方程是:

說明:雖然寫成花括號,但是它們的關係是 或者 。

初始化:dp[0][0] = false,因為是正整數,當然湊不出和為 00;

輸出:dp[len – 1][target],這裡 len 表示陣列的長度,target 是陣列的元素之和(必須是偶數)的一半。

/**

* 方法一

* @param nums

* @return

*/public boolean canpartition(int nums)

int sum = 0;

for (int num : nums)

//判斷:奇數 就不符合要求

if((sum&1) == 1)

int target = sum / 2;

//建立二維陣列,行:物品,列:揹包容量

boolean dp = new boolean[len][target+1];

//先 填第乙個物品 就是第一行,因為只有乙個物品,所以只能是剛好裝滿的那個是位置

//dp[0] 第乙個物品

if(nums[0] <= target)

//再填下面的行

for (int i = 1; i < len; i++)

if(nums[i] < j)}}

return dp[len-1][target];

}

複雜度分析:

時間複雜度:o(nc)o(nc):這裡 nn 是陣列元素的個數,cc 是陣列元素的和的一半。

空間複雜度:o(nc)o(nc)。

解釋設定 dp[0][0] = true 的合理性(重點):修改狀態陣列初始化的定義:dp[0][0] = true。考慮容量為 00 的時候,即 dp[i][0]。按照本意來說,應該設定為 false ,但是注意到狀態轉移方程(**中):dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i]];

當 j - nums[i] == 0 成立的時候,根據上面分析,就說明單獨的 nums[i] 這個數就恰好能夠在被分割為單獨的一組,其餘的數分割成為另外一組。因此,我們把初始化的 dp[i][0] 設定成為 true 是沒有問題的。

注意:觀察狀態轉移方程,or 的結果只要為真,** 這一列 下面所有的值都為真。因此在填表的時候,只要**的最後一列是 true,**就可以結束,直接返回 true。

/**

* 優化一

* @param nums

* @return

*/public boolean canpartition1(int nums)

int sum = 0;

for (int num : nums)

if ((sum & 1) == 1)

int target = sum / 2;

boolean dp = new boolean[len][target + 1];

// 初始化成為 true 雖然不符合狀態定義,但是從狀態轉移來說是完全可以的

dp[0][0] = true;

if (nums[0] == target)

for (int i = 1; i < len; i++)

}// 由於狀態轉移方程的特殊性,提前結束,可以認為是剪枝操作

if (dp[i][target])

}return dp[len - 1][target];

}

優化:

考慮空間優化(重要)

說明:這個技巧很常見、很基礎,請一定要掌握。

「0-1 揹包問題」常規優化:「狀態陣列」從二維降到一維,減少空間複雜度。

實際上,在「滾動陣列」的基礎上還可以優化,在「填**」的時候,當前行總是參考了它上面一行 「頭頂上」 那個位置和「左上角」某個位置的值。因此,我們可以只開乙個一維陣列,從後向前依次填表即可。

「從後向前」 寫的過程中,一旦 nums[i] <= j 不滿足,可以馬上退出當前迴圈,因為後面的 j 的值肯定越來越小,沒有必要繼續做判斷,直接進入外層迴圈的下一層。相當於也是乙個剪枝,這一點是「從前向後」填表所不具備的。

/**

* 優化二

* @param nums

* @return

*/public boolean canpartition2(int nums)

int sum = 0;

for (int num : nums)

//奇數

if((sum&1) == 1)

int target = sum /2;

//一維陣列

boolean dp = new boolean[target + 1];

//第乙個 取true;

dp[0] = true;

if(nums[0] <= target)

for(int i = 1;i < len ;i++)

dp[j] = dp[j] || dp[j - nums[i]];}}

return dp[target];

}

複雜度分析:

時間複雜度:o(nc)o(nc):這裡 nn 是陣列元素的個數,cc 是陣列元素的和的一半;

空間複雜度:o(c)o(c):減少了物品那個維度,無論來多少個數,用一行表示狀態就夠了。

416 分割等和子集

給定乙個只包含正整數的非空陣列。是否可以將這個陣列分割成兩個子集,使得兩個子集的元素和相等。注意 每個陣列中的元素不會超過 100 陣列的大小不會超過 200 示例 1 輸入 1,5,11,5 輸出 true 解釋 陣列可以分割成 1,5,5 和 11 示例 2 輸入 1,2,3,5 輸出 fals...

416 分割等和子集

主要題目中說了不超過100個數字,數字都不超過200。所以可能的和不會超過20000,這個量級對計算機來說不算大,所以考慮用dp考察每個可能的和是否存在。class solution int sum accumulate nums.begin nums.end 0 if sum 1 int siz ...

416 分割等和子集

題目描述 給定乙個只包含正整數的非空陣列。是否可以將這個陣列分割成兩個子集,使得兩個子集的元素和相等。注意 每個陣列中的元素不會超過 100 陣列的大小不會超過 200 示例 1 輸入 1,5,11,5 輸出 true 解釋 陣列可以分割成 1,5,5 和 11 示例 2 輸入 1,2,3,5 輸出...