问题
题目难易:中等
给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
注意: 每个数组中的元素不会超过 100 数组的大小不会超过 200
示例 1:
输入: [1, 5, 11, 5]
输出: true
解释: 数组可以分割成 [1, 5, 5] 和 [11].
示例 2:
输入: [1, 2, 3, 5]
输出: false
解释: 数组不能分割成两个元素和相等的子集.
提示:
1 <= nums.length <= 200
1 <= nums[i] <= 100
思路
问题分析:
- 计算数组元素总和sum
- 判断数组元素总和对2取余sum%2是否为0,为0继续,不为0说明无法实现,退出程序。
- 转化为背包问题,背包容量为sum/2,如果能正好装满,说明可以实现。否则说明无法实现,退出程序。
- 之后将数组的数依次放入背包,数字的大小也就是物品的重量,也是物品的价值。
使用动规五部曲分析:
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序
- 举例推导dp数组
1. 确定dp数组(dp table)以及下标的含义
采取一维dp数组:
dp[j]代表j的大小的背包里面,能存放的最大的数是多少,所有的数仅存放一遍。
2. 确定递推公式
两种状态:
- 不放物品i时: dp[j]=dp[j] (也就是之前数组的值不变)
- 放入物品i时: dp[j]=dp[j-num[i]]+num[i]
最后求取最大值
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
3. dp数组如何初始化
题目要求正整数数组,我们就可以把数组初始化为
// 题目中说:每个数组中的元素不会超过 100,数组的大小不会超过 200
// 总和不会大于20000,背包最大只需要其中一半,所以10001大小就可以了
vector<int> dp(10001, 0);
4. 确定遍历顺序
如果使用一维dp数组,物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒序遍历!
// 开始 01背包
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]);
}
}
5. 举例推导dp数组
dp[j]的数值一定是小于等于j的。
如果dp[j] == j 说明,集合中的子集总和正好可以凑成总和j,理解这一点很重要。
用例1,输入[1,5,11,5] 为例,如图:
C++代码如下:
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = 0;
// dp[i]中的i表示背包内总和
// 题目中说:每个数组中的元素不会超过 100,数组的大小不会超过 200
// 总和不会大于20000,背包最大只需要其中一半,所以10001大小就可以了
vector<int> dp(10001, 0);
for (int i = 0; i < nums.size(); i++) {
sum += nums[i];
}
// 也可以使用库函数一步求和
// int sum = accumulate(nums.begin(), nums.end(), 0);
if (sum % 2 == 1) return false;
int target = sum / 2;
// 开始 01背包
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]);
}
}
// 集合中的元素正好可以凑成总和target
if (dp[target] == target) return true;
return false;
}
};
总结
这道题目就是一道01背包应用类的题目,需要我们拆解题目,然后套入01背包的场景。
01背包相对于本题,主要要理解,题目中物品是nums[i],重量是nums[i],价值也是nums[i],背包体积是sum/2。
看代码的话,就可以发现,基本就是按照01背包的写法来的。