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.
------------------------------------------------------------------------------------------------------------------------
nums中的数都是非负整数
分治法:
nums is [1, 1, 1, 1, 1], S is 3的解
等价于nums=[1,1,1,1], S is 3-1=2 的解与 nums=[1,1,1,1], S is 3+1=4 的解的和
注意nums[0]=0, S=0时的解为2。0取正负号都是0
代码如下:
class Solution {
public int findTargetSumWays(int[] nums, int S) {
if(nums.length==1){
if(S==0) return nums[0]==0?2:0;
if(S!=0) return (nums[0]==S||-nums[0]==S)?1:0;
}
return findTargetSumWays_(nums,nums.length-2,S-nums[nums.length-1]) +
findTargetSumWays_(nums,nums.length-2,S+nums[nums.length-1]);
}
public int findTargetSumWays_(int[] nums,int end_idx,int S){
if(end_idx==0){
if(S==0) return nums[0]==0?2:0;
if(S!=0) return (nums[0]==S||-nums[0]==S)?1:0;
}
return findTargetSumWays_(nums,end_idx-1,S-nums[end_idx]) + findTargetSumWays_(nums,end_idx-1,S+nums[end_idx]);
}
}
由于各个数字都有正号和负号,总共有2**n种。时间复杂度为指数次,非常慢
------------------------------------------------------------------------------------------------------------------
第二种解法:
对于这个问题的解,有一部分的数前面是正号,有一部分的数前面是负号。
sum(P) - sum(N) = target
sum(P) + sum(N) + sum(P) - sum(N) = target + sum(P) + sum(N)
2 * sum(P) = target + sum(nums)
所以问题变成了,sum(p) = [target+sum(nums)] / 2
即对nums部分求和,加出sum(p)的方式有多少种?
这个问题与LeetCode 494题基本一样,只是这里是方式的个数。本质上它们都是0/1背包问题。
问题由指数次变为了线性时间复杂度。
注意到,可以用下面的三个条件快速排除一些不可能的情况:
target+sum(nums) 不可能为奇数
因为nums中的数都是非负的,所以:
target不可能大于sum(nums)
同理,target不可能小于-sum(nums)
public int findTargetSumWays(int[] nums, int s) {
int sum = 0;
for(int n : nums)
sum += n;
if(sum < s || s<-sum || (s + sum) % 2 != 0) return 0;
return subsetSum(nums, (s + sum)/2 );
}
现在,对于subsetSum的实现,采用动态规划,这其实是个0/1背包问题。
一开始,按照LeetCode 416. Partition Equal Subset Sum https://blog.youkuaiyun.com/qq_39638957/article/details/88902406 的思想,对dp的第一行和第一列都进行了初始化。
结果发现,第一列dp[:,0]的初始化还需要额外计算,代码如下:
//O(n**2)空间复杂度
public int subsetSum(int[] nums, int s){
int n = nums.length;
int[][] dp = new int[n+1][s+1];
dp[0][0] = 1;
for(int i = 1;i<n+1;i++){
dp[i][0] = get0ways(nums,i-1);
}
for(int j = 1;j<s+1;j++){
dp[0][j] = 0;
}
for(int i = 1;i<n+1;i++){
for(int j = 0;j<s+1;j++){
dp[i][j] = dp[i-1][j];
if(j-nums[i-1]>=0){
dp[i][j] += dp[i-1][j-nums[i-1]];
}
}
}
return dp[n][s];
}
private int get0ways(int[]nums,int i){
int count = 0;
for(int k = 0;k<=i;k++){
if(nums[k]==0){
count++;
}
}
return (int) Math.pow(2,count);
}
但是发现,事实上根本就不用对第一列进行初始化!!!每次只是用到了上一行的结果而已
事实上,在Partition Equal Subset Sum问题中,也根本就不用进行列的初始化。
改进代码如下:
//O(n**2)空间复杂度
public int subsetSum(int[] nums, int s){
int n = nums.length;
int[][] dp = new int[n+1][s+1];
dp[0][0] = 1;
for(int j = 1;j<s+1;j++){
dp[0][j] = 0;
}
for(int i = 1;i<n+1;i++){
for(int j = 0;j<s+1;j++){
dp[i][j] = dp[i-1][j];
if(j-nums[i-1]>=0){
dp[i][j] += dp[i-1][j-nums[i-1]];
}
}
}
return dp[n][s];
}
现在,可以进一步优化到,只使用两行的空间:
//只保留两行
public int subsetSum(int[] nums, int s){
int n = nums.length;
int[] prev = new int[s+1];
prev[0] = 1;
int[] cur = new int[s+1];
for(int i = 1;i<n+1;i++){//cur是dp的第i行
for(int j = 0;j<s+1;j++){
cur[j] = prev[j];
if(j-nums[i-1]>=0){
cur[j] += prev[j-nums[i-1]];
}
}
int[] temp = prev;
prev = cur;
cur = temp;
}
return prev[s];
}
进而,其实可以只使用一行的空间:
//只保留一行
public int subsetSum(int[] nums, int s){
int n = nums.length;
int[] cur = new int[s+1];
cur[0] = 1;
for(int i = 1;i<n+1;i++){//cur是dp的第i行
for(int j = s;j>=0;j--){
if(j-nums[i-1]>=0){
cur[j] += cur[j-nums[i-1]];
}
}
}
return cur[s];
}