题目描述
给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。(不要求连续)
示例:
输入: [1, 5, 11, 5]
输出: true
解释: 数组可以分割成 [1, 5, 5] 和 [11].
解题思路
看大佬题解,这又是一个01背包的变形。
问题翻译:给定一个只包含正整数的非空数组。是否可以从这个数组中挑选出一些正整数,每个数只能用一次,使得这些数的和等于整个数组元素的和的一半。
-
动态规划:
-
状态表示:
dp[i][j]
表示从数组的[0, i]
这个子区间内挑选一些正整数,每个数只能用一次,能否使得这些数的和等于j
。 -
状态转移方程:面对一个新来的数字
nums[i]
,我们有两种选择:选择/不选择- 若不选择:如果在
[0, i - 1]
这个子区间内已经有一部分元素,使得它们的和为j
,那么dp[i][j] = true
; - 若选择:如果在
[0, i - 1]
这个子区间内就能找到一部分元素,使得它们的和为j - nums[i]
,那么dp[i][j] = true
;(这里的前提是nums[i] <= j
)
所以最终的状态转移方程为:
dp[i][j] = dp[i - 1][j] or dp[i - 1][j - nums[i]], (nums[i] <= j)
。 - 若不选择:如果在
-
初始化:由状态转移方程可知,后面状态的更新需要前一行的状态(二维dp数组),所以我们需要初始化第一行的数据。
-
-
动态规划(状态空间压缩):方法和思想基本类似《剑指offer》第47题。上述
dp
解法状态空间是 O ( n 2 ) O(n^2) O(n2)。因为我们发现“从第 2 行开始,每一行都参考了前一行的当前位置的值,并且还参考了前一行的小于当前位置的值”。所以当我们在处理第i
行时,i-2
行及更上面的所有格子所记录的状态都没必要保存下来,所以我们可以将状态空间压缩到 O ( n ) O(n) O(n),用一维数组记录状态。
注:如果我们用一维dp
数组的话,必须从后往前填写。
参考代码
回溯(超时)
// 第一反应:回溯,但是妥妥超时
class Solution {
public:
bool canPartition(vector<int>& nums) {
int length = nums.size();
if(length == 0)
return false;
int target = accumulate(nums.begin(), nums.end(), 0);
if(target & 1) // 特判:如果是奇数,就不符合要求
return false;
target >>= 1;
sort(nums.begin(), nums.end());
int tmpSum = 0;
return dfs(nums, target, tmpSum, 0);
}
bool dfs(vector<int>& nums, int target, int tmpSum, int index){
if(index >= nums.size() || tmpSum > target)
return false;
if(tmpSum == target)
return true;
// 每个元素有两种选择:不选/选
return dfs(nums, target, tmpSum, index + 1) || dfs(nums, target, tmpSum + nums[index], index + 1);
}
};
二维dp(可AC)
class Solution {
public:
bool canPartition(vector<int>& nums) {
int length = nums.size();
if(length == 0)
return false;
int target = accumulate(nums.begin(), nums.end(), 0);
if(target & 1) // 特判 2:如果是奇数,就不符合要求
return false;
target >>= 1;
// 创建二维状态数组,行:物品索引,列:容量
int dp[length][target+1];
memset(dp, 0, sizeof(dp));
// 先写第 1 行(由状态转移方程可知,后面状态的更新需要前一行的状态(二维dp数组),所以我们需要初始化第一行的数据)
for(int i = 1; i <= target; i++){
if(nums[0] == i)
dp[0][i] = true;
}
for(int i = 1; i < length; i++){
for(int j = 1; j <= target; j++){ // j从0开始也可以,在这里没影响
dp[i][j] = dp[i-1][j];
if(j-nums[i] >= 0)
dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i]];
}
}
return dp[length-1][target];
}
};
// 这样也行(状态转移那里,数组的索引稍微变了下)
class Solution {
public:
bool canPartition(vector<int>& nums) {
int length = nums.size();
if(length == 0)
return false;
int target = accumulate(nums.begin(), nums.end(), 0);
if(target & 1) // 特判 2:如果是奇数,就不符合要求
return false;
target >>= 1;
// 创建二维状态数组,行:物品索引,列:容量
vector<vector<bool> > dp(length+1, vector<bool>(target+1, false));
dp[0][0] = true; // 重要
for(int i = 1; i <= length; i++){
for(int j = 1; j <= target; j++){ // j从0开始也可以,在这里没影响
dp[i][j] = dp[i-1][j];
if(j-nums[i-1] >= 0)
dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i-1]];
}
}
return dp[length][target];
}
};
一维dp(更快更省空间)
class Solution {
public:
bool canPartition(vector<int>& nums) {
int length = nums.size();
if(length == 0)
return false;
int target = accumulate(nums.begin(), nums.end(), 0);
if(target & 1) // 特判 2:如果是奇数,就不符合要求
return false;
target >>= 1;
// 创建一维状态数组
int dp[target+1];
memset(dp, 0, sizeof(dp));
// 先写第 1 行(由状态转移方程可知,后面状态的更新需要前一行的状态(二维dp数组),所以我们需要初始化第一行的数据)
for(int i = 1; i <= target; i++){
if(nums[0] == i){
dp[i] = true;
break; // 可以提前跳出
}
}
for(int i = 1; i < length; i++){
for(int j = target; j >= 1; j--){ // 用一维dp数组的话,必须从后往前填写
if(j >= nums[i])
dp[j] = dp[j] || dp[j - nums[i]];
}
}
return dp[target];
}
};