背包问题专项

476 阅读4分钟

目标和

题目

image.png

版本1 正确

    public int findTargetSumWays(int[] nums, int target) {

        // 数组中每个数字, 都可以选择是赋予正数还是负数, 目的是等于target
        // 统计有多少种方案
        // 可以看作是完全背包问题, 每个数字我可以放正数也可以放负数, 就相当于背包我可以选择放, 也可以选择不放
        // dp[i][j] 数组的定义就是, 一定使用前i个元素, 构成目标金额j的方案数目
        // dp[i][j] = dp[i - 1][j - nums[i]] + dp[i - 1][j + nums[i]]
        // 根据状态转移方程, 我们发现当前状态是和未来的j是有关的, 此时可以采用递归, 或者改写成递推的写法
        // 即dp[i][j + nums[i]] = dp[i - 1][j] + dp[i][j + nums[i]] 这里为什么要加上dp[i][j + nums[i]]呢?
        // 是因为j + nums[i] 的结果, 可能会由多个nums[i]得到, 因此必须加上之前的结果
        // 发现列存在减法的情况, 很有可能小于0, 因此需要为列增加空间
        // 根据题目, nums的和最大为1000, 也就是j最多就是减去1000
        // 因此相当于目标金额是把1000当作0的, 1000是作为基础值的
        int [][] dp = new int[nums.length][2001];

        // base case
        dp[0][nums[0] + 1000] = 1; // 第一个元素, 构成自己的正数方案数为1
        dp[0][1000 - nums[0]] += 1; // 第一个元素, 构成自己的负数方案数为 +=1 原因是第一个数可能为0

        for (int i = 1; i < nums.length; i ++) {
            for (int j = - 1000; j <= 1000; j ++) {
                // dp[i - 1][j + 1000]的判断就是大于0才有累加的意义, 不然也是加0
                if (dp[i - 1][j + 1000] > 0) {
                    dp[i][j + nums[i] + 1000] += dp[i - 1][j + 1000];
                    dp[i][j - nums[i] + 1000] += dp[i - 1][j + 1000];
                }
            }
        }

        return dp[nums.length - 1][target + 1000];

    }

正确的原因

(1) 注意目标金额那里全部都增加了1000

(2) 枚举目标金额在-1000到1000的所有情况

(3) 状态转移方程, 如何变成递推的方程

版本2 错误 将问题转化

    public int findTargetSumWays(int[] nums, int target) {

        // 数组中每个数字, 都可以选择是赋予正数还是负数, 目的是等于target
        // 统计有多少种方案
        // 假设为数组中所有元素都分配了正号和负号, 那么数组nums中的元素, 此时就为两部分, 一部分是正的, 一部分是负的
        // 此时正数部分的和假设为X, 负数部分的和假设为Y, 定义sum是数组元素全为正的时候的和(即原始nums数组的和)
        // 存在 X - Y = sum;
        // 那么假设此时数组是我们期望的数组 也存在 X + Y = target
        // 对X - Y = sum 以及X + Y = target进行变量替换, 将Y消除, 得到 x = (sum + target) / 2
        // 那么问题就变成在nums中, 寻找一个子序列数组, 满足该子序列的和x = (sum + target) / 2
        // 就变成了单纯的背包问题

        int sum = 0;
        for (int i = 0; i < nums.length; i ++) {
            sum += nums[i];
        }

        // 目标和不能大于sum, 否则全为正数也组成不了
        // 同时子序列的和x一定要是整数, 因此(target + sum) % 2一定要是整数
        if (target > sum || (target + sum) % 2 != 0) {
            return 0;
        }
        sum = (sum + target) / 2;

        // dp[i][j] 表示在数组nums中选择前i个元素, 能够构成j的种数
        // 注意这里前i个元素 也需要为dp多一个位置, 背包问题数组第一个位置都应该是nums.length + 1
        int [][] dp = new int [nums.length + 1][sum + 1];

        // base case  dp[i][0] = 1
        for (int i = 0; i < nums.length + 1; i ++) {
            dp[i][0] = 1;
        }

        for (int i = 1; i < nums.length + 1; i ++) {
            for (int j = 1; j < sum + 1; j ++) {
                if (j - nums[i - 1] >= 0) {
                    dp[i][j] = dp[i - 1][j - nums[i - 1]] + dp[i - 1][j];
                } else {
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }

        return dp[nums.length][sum];
    }

错误的原因

image.png

(1) 数组中可能会出现0, 但是正负0认为是两种情况, 因此需要在base case那里, 针对0的情况做处理

版本3 最优答案

    public int findTargetSumWays(int[] nums, int target) {

        // 数组中每个数字, 都可以选择是赋予正数还是负数, 目的是等于target
        // 统计有多少种方案
        // 假设为数组中所有元素都分配了正号和负号, 那么数组nums中的元素, 此时就为两部分, 一部分是正的, 一部分是负的
        // 此时正数部分的和假设为X, 负数部分的和假设为Y, 定义sum是数组元素全为正的时候的和(即原始nums数组的和)
        // 存在 X - Y = sum;
        // 那么假设此时数组是我们期望的数组 也存在 X + Y = target
        // 对X - Y = sum 以及X + Y = target进行变量替换, 将Y消除, 得到 x = (sum + target) / 2
        // 那么问题就变成在nums中, 寻找一个子序列数组, 满足该子序列的和x = (sum + target) / 2
        // 就变成了单纯的背包问题

        int sum = 0;
        for (int i = 0; i < nums.length; i ++) {
            sum += nums[i];
        }

        // 目标和不能大于sum, 否则全为正数也组成不了
        // 同时子序列的和x一定要是整数, 因此(target + sum) % 2一定要是整数
        if (target > sum || (target + sum) % 2 != 0) {
            return 0;
        }
        sum = (sum + target) / 2;

        // dp[i][j] 表示在数组nums中选择前i个元素, 能够构成j的种数
        // 注意这里前i个元素 也需要为dp多一个位置, 背包问题数组第一个位置都应该是nums.length + 1
        int [][] dp = new int [nums.length + 1][sum + 1];

        // base case  dp[i][0] = count
        int count = 1;
        dp[0][0] = 1; // 即[]
        for (int i = 1; i < nums.length + 1; i ++) {
            if (nums[i - 1]== 0) {
                // 如果某个元素为0, 那么[+0][-0]都满足最后重量为0的的条件
                // 如果元素不为0, 那么种数就和之前一致
                // 如果遇见第二个0, 种数就要继续乘2
                count *= 2; 
            }
            dp[i][0] = count;
        }

        for (int i = 1; i < nums.length + 1; i ++) {
            for (int j = 1; j < sum + 1; j ++) {
                if (j - nums[i - 1] >= 0) {
                    dp[i][j] = dp[i - 1][j - nums[i - 1]] + dp[i - 1][j];
                } else {
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }

        return dp[nums.length][sum];
    }

正确的原因

(1) 一定要注意base case的写法, 数组可能有0, 因此目标金额为0的情况是有不同的种数的, 而不是固定为1