LeetCode 494. Target Sum

本文探讨了给定一组非负整数及目标和S的情况下,通过分配符号+或-使整数之和等于S的方法数量。提供了两种解决方案:一种是递归分治法,时间复杂度较高;另一种是利用动态规划优化,将问题转化为0/1背包问题,大幅提高了效率。

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.

------------------------------------------------------------------------------------------------------------------------

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];
    }   

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值