"你的背包,不再让我走得缓慢"
416. 分割等和子集:一个 只包含正整数 的 非空(至少有一个元素) 数组 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。提示:,。
| 示例1 | 示例2 |
|---|---|
| 输入: 输出: 解释: 数组可以分割成 和 | 输入: 输出: 解释: 数组不能分割成两个元素和相等的子集。 |
中规中矩的 0-1 背包
从整体分析,题意要求的两个元素和相等的子集,不是子数组,因此不需要元素是连续的。
我们将这道题转换成 背包问题(参考 重识背包问题(上)),即
-
每个元素不能重复使用
-
背包体积为 (💥前置结论:如果 内所有元素之和为奇数,一定不能分割成两个元素和相等的子集)
-
背包中第 件物品的重量与价值均为
-
如果背包正好装满,说明可以分割成两个元素和相等的子集
1、确定 dp 数组及含义
💥 代表从 中取任意物品放进容量为 的背包后的物品总价值,其中 ,。
2、确定 dp 对应的状态方程
对于第 件物品,有两种状态,即放入背包或不放入背包。
-
主动不放入: 这种情况不受限于背包的容量,即
-
被动不放入: 这种情况下受限于背包的容量,不能再将 的物品放入背包,此时 ,那么状态转移方程就是
-
主动放入: 此时 ,要先从背包里腾出这个空间,再将 的物品放入背包,即
综上所述,令
3、确定 dp 初始状态
首先,当背包的承载为 ,总价值必定为 ;其次其次,只有第一件物品时,只要当前背包容量存在,,最大价值即为 ,否则是 。
4、确定遍历顺序
标准二维 的背包遍历顺序:
- 第一层循环: 从 到
- 第二层循环: 从 到
5、确定返回值
当从 中选取物品,放入容里为 的背包后,总价值恰为 时,即 ,说明当前数组 可以分为两个和一样的子数组。
6、示例代码
二维
/**
* 时间复杂度:O(n*target), 其中 n 是数组的长度, target 是整个数组的元素和的一半。
* 空间复杂度:O(n*target)
*/
function canPartition(nums: number[]): boolean {
const sum = nums.reduce((p, c) => p + c, 0); // 元素总和
if (sum & 1) { // 求奇偶性
return false;
}
const bagWeight = sum >> 1;
const length = nums.length;
const dp = Array.from({ length }, () => new Array(bagWeight + 1).fill(0));
for(let j = nums[0]; j <= bagWeight; j++) {
dp[0][j] = nums[0];
}
for(let i = 1; i < length; i++) {
for(let j = 1; j <= bagWeight; j++) {
if (j < nums[i]) {
dp[i][j] = dp[i - 1][j];
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - nums[i]] + nums[i]);
}
}
}
return dp[length - 1][bagWeight] === bagWeight;
};
状态压缩:💥此时 表示容量为 的背包所承载的最大价值。 能初始化为 的原因是, 中每个元素均为正整数,局部最小值可以取为 。
/**
* 时间复杂度:O(n*target), 其中 n 是数组的长度, target 是整个数组的元素和的一半。
* 空间复杂度:O(target)
*/
function canPartition(nums: number[]): boolean {
const sum = nums.reduce((p, c) => p + c, 0);
if (sum & 1) {
return false;
}
const bagWeight = sum >> 1;
const length = nums.length;
const dp = new Array(bagWeight + 1).fill(0);
for(let i = 1; i < length; i++) {
// 每个元素一定不能重复放入背包内,故要倒序遍历
for(let j = bagWeight; j >= nums[i]; j--) {
dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
}
}
return dp[bagWeight] === bagWeight;
};