給定乙個只包含正整數的非空陣列。是否可以將這個陣列分割成兩個子集,使得兩個子集的元素和相等。
注意:每個陣列中的元素不會超過 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 輸出...