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:
- The length of the given array is positive and will not exceed 20.
- The sum of elements in the given array will not exceed 1000.
- 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];
}
}