【背包问题系列】二、抽象背包问题

173 阅读2分钟

【背包问题系列】二、抽象背包问题

抽象背包问题

[题目描述]

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

注意:

每个数组中的元素不会超过 100
数组的大小不会超过 200
示例 1:

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

输出: true

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

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

输出: false

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

[思路以及题目分析]

题目分析

  • 两个元素和相等的子集,即每个子集的Sum-sum[1]=sum[1]=sum[2]=Sum/2,且所有值均为整数
  • 所以可以优先排除一种情况-所有元素和为奇数的情况不满足
  • 接下来的问题似乎看起来就可以很容易的抽象为背包问题
    • 有固定数量n的物品,总价值为total,是否能找到一种情况,选择物品满足物品和为 total/2

思路一:动态规划

  • 本题毫无疑问可以使用暴力法进行所有方法匹配但是一定会TLE,也就不写这种办法了

  • 在第一时间难以想象到dp方程的时候可以考虑递归方式进行参数确认

  • 递归求解的时候,如果正向求解,则每次增加一个物品,然后判断前i是否能满足要求j(最大值不超过j)的最大价值

  • 因此可以定义dp方程dp[i][j] 表示使用前i个元素组成不超过j最大价值

  • dp[i][j] = Math.max(dp[i - 1][j], j >= val ? dp[i - 1][j - val] + val : 0)

public boolean canPartition(int[] nums) {
            int total = 0;
            for (int n : nums
            ) {
                total += n;
            }
            //corner case
            if (total % 2 != 0) {
                return false;
            }
            int target = total / 2 + 1;
            int[][] dp = new int[nums.length][target];
            //初始化边界情况,只取第一个元素的所有情况
            for (int i = 0; i < target; i++) {
                dp[0][i] = nums[0] < i ? nums[0] : 0;
            }
            for (int i = 1; i < nums.length; i++) {
                int val = nums[i];
                for (int j = 0; j < target; j++) {
                    //选或者不选的两种状态
                    dp[i][j] = Math.max(dp[i - 1][j], j >= val ? dp[i - 1][j - val] + val : 0);
                }
            }
            return dp[nums.length - 1][target - 1] == target - 1;
        }

时间复杂度O(n+n*total)

其余维度优化方式和【背包问题系列一】很相似

优化一 维度抽象成奇偶

public boolean canPartition(int[] nums) {
            int total = 0;
            for (int n : nums
            ) {
                total += n;
            }
            //corner case
            if (total % 2 != 0) {
                return false;
            }
            int target = total / 2 + 1;
            int[][] dp = new int[2][target];
            //初始化边界情况,只取第一个元素的所有情况
            for (int i = 0; i < target; i++) {
                dp[0][i] = nums[0] < i ? nums[0] : 0;
            }
            for (int i = 1; i < nums.length; i++) {
                int val = nums[i];
                for (int j = 0; j < target; j++) {
                    //选或者不选的两种状态
                    dp[i & 1][j] = Math.max(dp[(i - 1) & 1][j], j >= val ? dp[(i - 1) & 1][j - val] + val : 0);
                }
            }
            return dp[(nums.length-1)&1][target - 1] == target - 1;
        }

优化二 维度抽象为一维

public boolean canPartition(int[] nums) {
            int total = 0;
            for (int n : nums
            ) {
                total += n;
            }
            //corner case
            if (total % 2 != 0) {
                return false;
            }
            int target = total / 2 + 1;
            int[] dp = new int[target];
            for (int i = 0; i < nums.length; i++) {
                int val = nums[i];
                for (int j = target - 1; j >= 0; j--) {
                    int no = dp[j];
                    // 选第 i 件物品
                    int yes = j >= val ? dp[j - val] + val : 0;
                    dp[j] = Math.max(no, yes);
                }
            }
            return dp[target - 1] == target - 1;
        }
    }

【参考链接】