Loading

Medium | LeetCode 416. 分割等和子集 | 0-1背包问题

416. 分割等和子集

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

注意:

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

示例 1:

输入: [1, 5, 11, 5]

输出: true

解释: 数组可以分割成 [1, 5, 5] 和 [11].

示例 2:

输入: [1, 2, 3, 5]

输出: false

解释: 数组不能分割成两个元素和相等的子集.

方法一: 暴力搜索

枚举所有的子集, 判断其子集的所有和是否是target/2。

方法二: 0-1 背包问题

问题可转化为在若干个物品中选出一些物品, 每个物品只能使用一次, 这些物品恰好能够填满容量为sum/2的背包。

经典的0-1背包问题: 0-1背包问题:在M件物品取出若干件放在体积为W的背包里,每件物品只有一件,它们有各自的体积 和价值,问如何选择使得背包能够装下的物品的价值最多。

动态规划的思路:一个一个物品去尝试,一点一点扩大考虑能够容纳的容积的大小,整个过程就像是在填写一张二维表格。

-设置状态:dp[i] [j]表示考虑下标[0, i]这个区间里的所有整数,在它们当中是否能够选出一些数,使得这些数之和恰好为整数j

-状态转移方程:

  1. 不选择 nums[i]: dp[i] [j] = dp[i - 1] [j];

  2. 选择 nums [i]:
    ① nums [i] == j, dp[i] [j] = true;
    ② nums[i] < j, dp[i] [j] = dp[i - 1] [j - nums [i]];

递归

根据以上的状态转移方程, 可以写出如下的递归的代码

public boolean canPartition(int[] nums) {
    int target = 0;
    for(int num: nums) {
        target += num;
    }
    if (target % 2 == 0) {
        target /= 2;
    } else {
        return false;
    }
    return knapsack(nums, target) == 1;
}

private int[][] dp;

public int knapsack(int[] value, int w) {
    dp = new int[value.length+1][w+1];
    return knapsackDp(value, w, value.length - 1);
}

/**
 * @param value  每个物品的价值
 * @param w      包的最大承重
 * @param index  在前index个物品中选择物品装进背包中
 * @return 装载前index个物品进w承重的包的最大价值
 */
private int knapsackDp(int[] value, int w, int index) {
    // 递归出口(前0件商品, 也就是第一件商品装进背包承重为w的最大价值)
    if (w < 0) {
        return -1;
    }
    if (index == 0) {
        return (w == 0) ?  1 : -1;
    }
    if (dp[index][w] != 0) {
        return dp[index][w];
    }
    return dp[index][w] = Math.max(
            // 把第index件物品装进背包, 即等价于于【把前index-1件物品装进重量为w - weight[index]的背包能获得的最大价值 + 当前index的价值】
            knapsackDp(value, w - value[index], index - 1),
            // 第index件物品不装进背包, 即等价于【把前index-1件物品装进重量为w的背包能获得的最大价值】
            knapsackDp(value, w, index - 1)
    );
}

迭代

public boolean canPartition(int[] nums) {
    int n = nums.length;
    if (n < 2) {
        return false;
    }
    int sum = 0, maxNum = 0;
    // 先对所有数字求和
    for (int num : nums) {
        sum += num;
        maxNum = Math.max(maxNum, num);
    }
    // 判断奇偶性, 如果是奇数, 直接返回false
    if (sum % 2 != 0) {
        return false;
    }
    // 获取得到target的值
    int target = sum / 2;
    if (maxNum > target) {
        return false;
    }
    
    boolean[][] dp = new boolean[n][target + 1];
    // 填充第0列的值, 全部填充为true
    for (int i = 0; i < n; i++) {
        dp[i][0] = true;
    }
    // 填充第0行值
    dp[0][nums[0]] = true;
    
    for (int i = 1; i < n; i++) {
        // 从第1行开始从左往右, 从上往下开始遍历
        int num = nums[i];
        for (int j = 1; j <= target; j++) {
            if (j >= num) {
                // 当前背包容量 大于 当前的体积, 
                // 那么可有 不把当前物品装进背包dp[i - 1][j]
                // 把当前物品装进背包两种选择
                dp[i][j] = dp[i - 1][j] | dp[i - 1][j - num];
            } else {
                // 当前背包容量小于物品体积, 那么只能选择将当前物品不装进背包
                dp[i][j] = dp[i - 1][j];
            }
        }
    }
    return dp[n - 1][target];
}

优化空间复杂度

第二层的循环我们需要从大到小计算,因为如果我们从小到大更新dp 值,那么在计算 dp[j] 值的时候,dp[j−nums[i]] 已经是被更新过的状态,不再是上一行的dp 值。

public boolean canPartition(int[] nums) {
    int n = nums.length;
    if (n < 2) {
        return false;
    }
    int sum = 0, maxNum = 0;
    for (int num : nums) {
        sum += num;
        maxNum = Math.max(maxNum, num);
    }
    if (sum % 2 != 0) {
        return false;
    }
    int target = sum / 2;
    if (maxNum > target) {
        return false;
    }
    boolean[] dp = new boolean[target + 1];
    dp[0] = true;
    for (int i = 0; i < n; i++) {
        int num = nums[i];
        for (int j = target; j >= num; --j) {
            dp[j] |= dp[j - num];
        }
    }
    return dp[target];
}

posted @ 2021-01-26 14:49  反身而诚、  阅读(72)  评论(0编辑  收藏  举报