分割等和子集
题目描述
给你一个 只包含正整数 的非空数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1:
text
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。示例 2:
text
输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。思路拆解
这道题可以转化为 0-1 背包问题。
问题转化
- 如果数组的总和
sum是奇数,那么不可能分割成两个相等的子集,直接返回false。 - 如果
sum是偶数,我们的目标是从数组中选出一些数字,使得它们的和恰好等于target = sum / 2。 - 这就变成了:背包容量为
target,物品是nums中的数字,每个物品只能用一次,问是否能恰好填满背包。
动态规划
- 定义状态:
dp[i]表示容量为i的背包是否能被恰好填满。 - 状态转移方程:
- 对于每个数字
num,我们需要更新dp数组。 - 为了保证每个数字只被使用一次,我们需要 倒序遍历 背包容量(从
target到num)。 dp[i] = dp[i] || dp[i - num]。- 意思是:如果不选
num,状态取决于dp[i](之前的数字能不能填满i);如果选num,状态取决于dp[i - num](之前的数字能不能填满i - num)。
- 对于每个数字
- 初始条件:
dp[0] = true(容量为 0 时,不选任何数字即可填满)。- 其他初始化为
false。
- 返回值:
dp[target]。
代码实现
javascript
/**
* @param {number[]} nums
* @return {boolean}
*/
var canPartition = function(nums) {
let sum = 0;
for (const num of nums) sum += num;
// 如果总和是奇数,无法平分
if (sum % 2 !== 0) return false;
const target = sum / 2;
const dp = new Array(target + 1).fill(false);
dp[0] = true;
for (const num of nums) {
// 倒序遍历,保证每个数字只使用一次
for (let i = target; i >= num; i--) {
dp[i] = dp[i] || dp[i - num];
}
}
return dp[target];
};运行演示
假设 nums = [1, 5, 11, 5],sum = 22,target = 11。
- 初始化
dp长度 12,dp[0]=true,其余false。 num = 1:i从 11 到 1:dp[1] = dp[1] || dp[0] = truedp变为[T, T, F, ...](T=true, F=false)
num = 5:i从 11 到 5:dp[6] = dp[6] || dp[1] = true(1+5=6)dp[5] = dp[5] || dp[0] = truedp中 T 的索引:0, 1, 5, 6
num = 11:i从 11 到 11:dp[11] = dp[11] || dp[0] = true- 找到目标
dp[11]为 true,其实可以提前返回。
num = 5:- 继续更新...
最终 dp[11] 为 true。
复杂度分析
- 时间复杂度:$O(n \times target)$,其中 $n$ 是数组长度,$target$ 是总和的一半。
- 空间复杂度:$O(target)$,需要一个长度为 $target$ 的数组。
