给定一个 只包含正整数 的 非空 数组。
是否可以将这个数组分割成两个子集,使得两个子集的元素和相等

注意:

  1. 每个数组中的元素不会超过 100
  2. 数组的大小不会超过 200

输入: [1, 5, 11, 5]
输出: true
解释: 数组可以分割成 [1, 5, 5] 和 [11]

输入: [1, 2, 3, 5]
输出: false
解释: 数组不能分割成两个元素和相等的子集.

分割等和子集 - 图1

我们需要对这个问题做一个等价转换。题目问我们:在输入数组中是否存在一个子集(子集非空,且每个元素至多只能选择一次),子集中所有元素的和为输入数组所有元素的和的一半。

这其实是一个典型的「0-1 背包问题」,我们将题目的条件和问的结果与「0-1 背包问题」的联系画出表格如下。

「力扣」第 416 题 0-1 背包问题
数组中的元素 可以选择的物品
每个元素或者选入子集,或者不选入子集 每个物品只能被选入背包 1 次
子集的和恰好为输入数组所有元素的和的一半 恰好装入总重量为输入数组所有元素的和的一半的背包

一个比较容易判断的前提条件:如果输入数组所有元素的和为奇数,一定找不到这样的子集
注意:

  • 当前问题要求背包 恰好能够装满 总重量为 j 的背包,需要注意初始化的条件:nums[0] 只能填满容积为 nums[0] 的背包
  • 以后我们在求解这类问题的时候,一定要区分清楚「恰好能够装满」和「装入容量不超过 **j** 的背包」的区别,我们在后面的例题中还会和大家强调这样的细节
  1. /**
  2. * @param {number[]} nums
  3. * @return {boolean}
  4. */
  5. var canPartition = function (nums) {
  6. // 求所有数的总和
  7. let sum = nums.reduce((acc, num) => acc + num);
  8. if (sum % 2 !== 0) {
  9. return false;
  10. }
  11. const len = nums.length;
  12. const target = sum / 2;
  13. // dp[i][j]:前缀区间 [0..i] 里是否有一部分元素的和恰好为 j
  14. const dp = Array.from(nums).map(() =>
  15. new Array(target + 1).fill(false)
  16. );
  17. // 初始化:nums[0] 只能填满容积为 nums[0] 的背包
  18. if (nums[0] <= target) {
  19. dp[0][nums[0]] = true;
  20. }
  21. for (let i = 1; i < len; i++) {
  22. for (let j = 0; j <= target; j++) {
  23. if (nums[i] > j) {
  24. // 不选:把上一行照抄下来
  25. dp[i][j] = dp[i - 1][j];
  26. } else {
  27. // 选:二者之中有一个为 true 即可
  28. dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i]];
  29. }
  30. }
  31. }
  32. return dp[len - 1][target];
  33. };

空间优化

  1. 根据「0-1 背包」问题表格复用的优化方法,我们可以采用「逆序填表」的方式将空间复杂度优化到 O(target)

    1. var canPartition = function (nums) {
    2. const sum = nums.reduce((acc, num) => acc + num);
    3. if (sum % 2 !== 0) return false;
    4. const target = sum / 2;
    5. const len = nums.length;
    6. const dp = new Array(target + 1).fill(false);
    7. dp[0] = true;
    8. for (let i = 0; i < len; i++) {
    9. for (let j = target; j >= nums[i]; j--) {
    10. if (dp[target]) return true // 提前返回结果
    11. dp[j] = dp[j] || dp[j - nums[i]];
    12. }
    13. }
    14. return dp[target];
    15. };