目录
给你一个 只包含正整数 的 非空 数组 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
思路:
本题要我们判断是否能将一个集合刚好分成两个总和相等的子集,首先我们可以算出总和,然后看能否被2整除
- 如果不能被2整除,那必然无法分成两个总和相等的子集,直接返回false。
- 如果能被2整除,我们就尝试能否找到一个集合,总和为sum/2,找到就返回true,找不到就返回false
那怎么找呢,这里就可以将这个问题转化成一个01背包问题,首先01背包问题的数组dp[i][j]表示的是表示在0-i个物品中,往容量为j的背包,放价值最大的物品。在本题中,集合中的数据就可以看成是一个个的物品,物品的重量和价值都是数据本身的值,所以翻译过来就是,
在0-i个数据中,往容量为j的背包,放价值最大的数据(数据的重量和价值都是数据本身的值)。
这里我们要求的是当背包容量为sum/2时,可以往背包中放的数据的最大总和。首先,这个总和肯定是<=j的(因为数据的重量和价值都是相等的,而背包容量就是能承载的最大重量),所以如果这个总和=j,那就说明找到了一个集合,元素的总和为sum/2,那就可以分成两份,返回true,否则返回false。
初始化,递推表达式,和01背包是完全一样的。
关于什么是01背包问题(如果知道,可以跳过下面部分的内容)
什么是01背包问题
有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
解决方法:使用动态规划的思想
1.dp[i][j]的含义
表示第0-i个物品放入背包容量为j的背包中可以获得的最大价值
2.递推公式。
dp[i][j]相比dp[i-1][j]的差别就是可以放入物品i,所以这里分类讨论
(1).如果i的重量比j更大,那i就无法放入背包,dp[i][j] = dp[i-1][j]
(2).如果i的重量比j小,i就可以选择是否放入背包,分两种情况讨论:
1).如果不放入物品i,那dp[i][j]就=dp[i-1][j](//放了物品i,其他物品可能就放不下了)
2).如果放入物品i,那dp[i][j]就 = dp[i-1][j-weight[i]]+value[i](也就是不放i且放入i以后背包刚好装满的情况)
两种策略,哪个价值大选哪个
所以,递推公式就出来了
if (j < weight[i]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
3.初始化,初始化二维数组的第一行和第一列,第一行就是只放第一个物品的时候,不同背包容量中,物品的最大价值(达到能放入物品的背包容量后,全是当前单个物品的价值)。第一列就是背包容量为0的时候,物品的最大价值(全是0)
01背包问题空间复杂度优化(滚动数组)
1.我们可以考虑只用一个一维数组dp[j],来表示表示第0-i个物品放入背包容量为j的背包中可以获得的最大价值(循环更新i+1次)。而我们可以重复利用这个数组的最关键因素就是:除了需要初始化的那些 dp 元素,其余所有的 dp[i][j] 都是通过其“前面”的元素计算得到的,或者说新的 dp[i][j] 会覆盖前一轮计算得到的 dp[i][j] ,所以一维数组中的某一个位置能够被重复利用。
2.原先二维的dp[i][k]的值实际上是根据dp[i-1][k]的值得出来的,所以我们可以将dp[i][k]的计算结果覆盖dp[i-1][k]
原来的递推关系:dp[i-1][j-weight[i]],也就是会用到前一层j之前的数据,所以我们更新当前dp[i][k]的时候应该从后往前更新(如果从前往后更新,后面用到的可能就是我们更新完的当前层的数据了,那就会出错)
if (j < weight[i]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
因此,我们得出最终的递推关系式:
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
3.初始化,可以初始化为第一行,从第1个物品开始遍历(i = 1);也可以不初始化(默认全是0),从第0个物品开始遍历(i = 0)
核心遍历部分:
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
本题代码:
class Solution {
public:
bool canPartition(vector<int>& nums) {
vector<vector<int>> dp(nums.size(), vector<int>(10005, 0));
//dp[i][j]表示在0-i个数据的集合中,往容量为j的背包,放价值最大的数据(数据的重量和价值都是数据本身的值)
int sum = 0;
for (auto num : nums) {
sum += num;
}
if (sum % 2 != 0) { //如果总和不能整除2,则必然无法分成两个总和相等的数据子集
return false;
}
int target = sum / 2;
for (int j = 0; j <= target; j++) { //初始化行
if (j >= nums[0]) { //这里的nums[j]表示物品重量
dp[0][j] = nums[0]; //这里的nums[j]表示物品价值
}
}
for (int i = 0; i < nums.size(); i++) { //初始化列
dp[i][0] = 0;
}
for (int i = 1; i < nums.size(); i++) {
for (int j = 1; j <= target; j++) {
if (j < nums[i]) { //如果j < nums[i],表示当前背包放不下i这个数据
dp[i][j] = dp[i - 1][j];
} else {
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - nums[i]] + nums[i]);
}
}
}
return dp[nums.size() - 1][target] == target; //如果相等,表示在大小为j的背包
//中,可以放下的数据子集的最大总和为j(这个最大总和肯定是<=j的,
//因为数据的重量和价值是相等的),也就是找到了一个总和为j的子集
}
};
本题空间复杂度优化(滚动数组)
class Solution {
public:
bool canPartition(vector<int>& nums) {
vector<int> dp(10005, 0);
//dp[i][j] 0-i个物品,当容量为j的时候最大v
//这里物品是数字,j是容量,v是数字对应的值,w是也是数字对应的值
//我们要求的是dp[j],也就是能凑成的子集总和<=j的最大的子集总和,如果总和=dp[j]表示找到了一个总和为j的子集
int sum = 0;
for (auto num : nums) {
sum += num;
}
if (sum % 2 != 0) {
return false;
}
int n = sum / 2;
for (int i = 0; i < nums.size(); i++) {
for (int j = n; j >= nums[i]; j--) {
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
}
}
return dp[n] == n;
}
};