leetcode494. 目标和

// 01背包
/**
 * 我们可以将原问题转化为: 找到nums一个正子集和一个负子集,使得总和等于target,统计这种可能性的总数。
 * 我们假设P是正子集,N是负子集。让我们看看如何将其转换为子集求和问题:
 *                   sum(P) - sum(N) = target
 *                  (两边同时加上sum(P)+sum(N))
 * sum(P) + sum(N) + sum(P) - sum(N) = target + sum(P) + sum(N)
 *             (因为 sum(P) + sum(N) = sum(nums))
 *                         2 * sum(P) = target + sum(nums) 
 * 因此,原来的问题已转化为一个求子集的和问题: 找到nums的一个子集 P,使得
 *          sum(P)= target+sum(nums) / 2
 * 根据公式,若target + sum(nums)不是偶数,就不存在答案,即返回0个可能解。
 * 因此题目转化为01背包,也就是能组合成容量为sum(P)的方式有多少种,一种组合中每个数字只能取一次。
 * 解决01背包问题使用的是动态规划的思想。
 * 
 * 方法是: 
 * 开辟一个长度为P+1的数组,命名为dp
 * dp的第x项,代表组合成数字x有多少方法。比如说,dp[0] = 1,代表组合成0只有1中方法,即什么也不取。
 * 比如说dp[5] = 3 ,代表使总和加到5总共有三种方法。
 * 所以最后返回的就是dp[P],代表组合成P的方法有多少种
 * 
 * 怎么更新dp数组呢?
 *      遍历nums,遍历的数记作num
 *          再逆序遍历从P到num,遍历的数记作j
 *              更新dp[j] = dp[j - num] + dp[j]
 * 这样遍历的含义是,对每一个在nums数组中的数num而言,dp在从num到P的这些区间里,
 * 都可以加上一个num,来到达想要达成的P。
 * 举例来说,对于数组[1,2,3,4,5],想要康康几种方法能组合成4,那么设置dp[0]到dp[4]的数组
 * 假如选择了数字2,那么dp[2:5](也就是2到4)都可以通过加上数字2有所改变,
 * 而dp[0:2](也就是0到1)加上这个2很明显就超了,就不管它。
 * 以前没有考虑过数字2,考虑了会怎么样呢?就要更新dp[2:5],比如说当我们在更新dp[3]的时候,
 * 就相当于dp[3] = dp[3] + dp[1],即本来有多少种方法,加上去掉了2以后有多少种方法。
 * 因为以前没有考虑过2,现在知道,只要整到了1,就一定可以整到3。
 * 
 * 为什么以这个顺序来遍历呢?
 * 假如给定nums = [num1,num2,num3],我们现在可以理解dp[j] = dp[j-num1] + dp[j-num2] + dp[j-num3]。
 * 但是如何避免不会重复计算或者少算?要知道,我们的nums并不是排序的,我们的遍历也不是从小到大的。
 * 我们不妨跟着流程走一遍
 *      第一次num1,仅仅更新了dp[num1] = 1,其他都是0+0都是0啊都是0
 *      第二次num2,更新了dp[num2] = 1和dp[num1+num2] = dp[num1+num2] + dp[num1] = 1,先更新后者。
 *      第三次num3,更新了dp[num3] = 1和dp[num1+num3] += 1和dp[num2+num3] += 1和dp[num1+num2+num3] += 1。
 *      按下标从大到小顺序来更新。
 */
class Solution {
    public int findTargetSumWays(int[] nums, int S) {
        if (nums == null || nums.length == 0) return 0;
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }
        if (sum < S || (S + sum) % 2 == 1) return 0;
        int p = (S + sum) / 2;
        int[] dp = new int[p + 1];
        // 代表组合成0只有1中方法,即什么也不取。
        dp[0] = 1;
        for (int num : nums) {
            for (int i = p; i >= num; i--) {
                dp[i] += dp[i - num];
            }
        }
        return dp[p];
    }
}

// 动态规划
/**
 * 定义状态
 * 搞清楚需要输出的结果后,就可以来想办法画一个表格,也就是定义dp数组的含义。
 * 根据背包问题的经验,可以将dp[ i ][ j ]定义为从数组nums中 0 - i 的元素进行加减可以得到 j 的方法数量。
 * 
 * 状态转移方程
 * 搞清楚状态以后,我们就可以根据状态去考虑如何根据子问题的转移从而得到整体的解。
 * 这道题的关键不是nums[i]的选与不选,而是nums[i]是加还是减,那么我们就可以将方程定义为:
 * dp[ i ][ j ] = dp[ i - 1 ][ j - nums[ i ] ] + dp[ i - 1 ][ j + nums[ i ] ]
 * 可以理解为nums[i]这个元素我可以执行加,还可以执行减,那么我dp[i][j]的结果值就是加/减之后对应位置的和。
 */
class Solution {
    public int findTargetSumWays(int[] nums, int S) {
        int sum = 0;
        for (int i = 0; i < nums.length; i++) {
            sum += nums[i];
        }
        // 绝对值范围超过了sum的绝对值范围则无法得到
        if (Math.abs(S) > Math.abs(sum)) return 0;

        int len = nums.length;
        // - 0 +
        int t = sum * 2 + 1;
        int[][] dp = new int[len][t];
        // 初始化
        if (nums[0] == 0) {
            dp[0][sum] = 2;
        }
        else {
            dp[0][sum + nums[0]] = 1;
            dp[0][sum - nums[0]] = 1;
        }

        for (int i = 1; i < len; i++) {
            for (int j = 0; j < t; j++) {
                // 边界
                int l = (j - nums[i]) >= 0 ? j - nums[i] : 0;
                int r = (j + nums[i]) < t ? j + nums[i] : 0;
                dp[i][j] = dp[i - 1][l] + dp[i - 1][r];
            }
        }
        return dp[len - 1][sum + S];
    }
}

你可能感兴趣的:(leetcode)