494. Target Sum

Description

You are given a list of non-negative integers, a1, a2, ..., an, and a target, S. Now you have 2 symbols + and -. For each integer, you should choose one from + and - as its new symbol.

Find out how many ways to assign symbols to make sum of integers equal to target S.

Example 1:

Input: nums is [1, 1, 1, 1, 1], S is 3.
Output: 5
Explanation:
-1+1+1+1+1 = 3
+1-1+1+1+1 = 3
+1+1-1+1+1 = 3
+1+1+1-1+1 = 3
+1+1+1+1-1 = 3

There are 5 ways to assign symbols to make the sum of nums be target 3.

Note:

  1. The length of the given array is positive and will not exceed 20.
  2. The sum of elements in the given array will not exceed 1000.
  3. Your output answer is guaranteed to be fitted in a 32-bit integer.

Solution

别被这道题吓住,仔细审一下题会发现,虽然这道题可能会有重复数字出现,但每个数字的符号选择都是独立的,并不像permutation要考虑重复组合的情况。所以这道题好做很多。

一开始看到只有不超过20个元素时,马上想到用bit-manipulation去做,即用二进制表示每组选择。但是这样做会TLE。

后来发现此题目挖了一个坑,重点不是元素数量,而是总和的范围不超过1000。根据这个条件可以采用空间换时间的策略。用memo[i][sum]代表从i开始总和为sum的ways count。注意sum需要加上1000的偏移以处理负值的情况。

Recursion with memo, time O(sum * n), space O(sum * n)

class Solution {
    public int findTargetSumWays(int[] nums, int S) {
        int[][] memo = new int[nums.length][2001];
        for (int[] row : memo) {
            Arrays.fill(row, -1);
        }
        
        return findWaysRecur(nums, 0, 0, S, memo);
    }
    
    public int findWaysRecur(int[] nums, int i, int sum, int target, int[][] memo) {
        if (i == nums.length) {
            return sum == target ? 1 : 0;
        }
        
        int index = sum + 1000;
        if (memo[i][index] > -1) {
            return memo[i][index];
        }
        
        int add = findWaysRecur(nums, i + 1, sum + nums[i], target, memo);
        int sub = findWaysRecur(nums, i + 1, sum - nums[i], target, memo);
        memo[i][index] = add + sub;
        
        return memo[i][index];
    }
}

DP, time O(sum * n), space O(sum * n)

根据上面的思路可以将解法轻松转换成DP。另外hardcode 1000的方式不好看,不如下面这种写法:

class Solution {
    public int findTargetSumWays(int[] nums, int S) {
        int sum = 0;
        for (int n : nums) {
            sum += n;
        }
        
        if (S < -sum || S > sum) {  // check for edge case
            return 0;
        }
        
        int n = nums.length;
        int[][] dp = new int[n + 1][2 * sum + 1];
        dp[n][sum] = 1;    // add sum as offset
        
        for (int i = n - 1; i >= 0; --i) {
            for (int j = 0; j < dp[i + 1].length; ++j) {
                if (dp[i + 1][j] == 0) {
                    continue;
                }
                
                dp[i][j + nums[i]] += dp[i + 1][j];
                dp[i][j - nums[i]] += dp[i + 1][j];
            }
        }
        
        
        return dp[0][S + sum];
    }
}

Subset sum, time O(sum * n), space O(sum)

这道题可以转换成从集合中选出子集P做加法,剩下的补集Q做减法,使得总和为target。那么则有:

  • sum(P) + sum(Q) = sum
  • sum(P) - sum(Q) = target
  • sum(P) = (sum + target) / 2

利用这个结论,使用DP去做就行了。下面是采用01背包思路的解法。

class Solution {
    public int findTargetSumWays(int[] nums, int S) {
        int sum = 0;
        for (int n : nums) {
            sum += n;
        }
        
        if (S < -sum || S > sum || ((sum + S) & 1) > 0) {
            return 0;
        }
        
        return subsetSum(nums, (sum + S) / 2);
    }
    
    public int subsetSum(int[] nums, int sum) {
        int[] dp = new int[sum + 1];
        dp[0] = 1;
        
        for (int n : nums) {
            for (int s = sum; s >= n; --s) {
                dp[s] += dp[s - n];
            }
        }
        
        return dp[sum];
    }
}

你可能感兴趣的:(494. Target Sum)