Leetcode——494. Target Sum

题目描述

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.

题目分析与解决方案

题目的意思大致就是数组中的每个数都可以是正数或者负数,将数组中的所有正数和负数求和,看有多少种组合能满足求和的结果等于target S。

法一:暴力递归求解

最容易想到的方法就是暴力求解,直接使用递归遍历所有可能的组合,从中挑出满足求和等于target S的组合。
递归树
相当于遍历这一棵二叉树,树的高度为N=nums.size(),遍历的总操作数 1 + 2 + 4 + … … + 2 n 1+2+4+……+2^n 1+2+4++2n,时间复杂度为 O ( 2 n ) O(2^n) O(2n)。相关代码就是代码部分的法一。这个方法容易理解,也容易实现,代码较少,就是效率不咋地。

法二:目标转移+动态规划

这个方法比较难想,我是在大神的博客上看到的。我感觉最起码得对动态规划比较熟悉,而且可能做过相类似的题才可能想得到。

首先设P为nums中的所有正数,N为nums中的所有负数,于是有

s u m ( P ) − s u m ( N ) = s u m ( n u m s ) , s u m ( P ) + s u m ( N ) = t a r g e t sum(P)-sum(N)=sum(nums),sum(P)+sum(N)=target sum(P)sum(N)=sum(nums)sum(P)+sum(N)=target
两式相加,得到

2 ∗ s u m ( P ) = s u m ( n u m s ) + t a r g e t = > s u m ( P ) = ( s u m ( n u m s ) + t a r g e t ) / 2 2*sum(P)=sum(nums)+target \\ =>sum(P)=(sum(nums)+target)/2 2sum(P)=sum(nums)+target=>sum(P)=(sum(nums)+target)/2

根据上述式子可知,如果sum(nums)+target不为偶数(2*整数的结果必然等于偶数),或者sum(nums)<target(这种情况下nums中的元素全为正,求和都小于target,无解),则必然无解。

所以接下来的目标就是 s u m ( P ) = ( s u m ( n u m s ) + t a r g e t ) / 2 sum(P)=(sum(nums)+target)/2 sum(P)=(sum(nums)+target)/2,也就是在数组中找到任意的数,组合起来之后求和等于 ( s u m ( n u m s ) + t a r g e t ) / 2 (sum(nums)+target)/2 (sum(nums)+target)/2。(只需考虑他们为正数的情况,也就是只考虑+的情况,因为求和的是 s u m ( P ) sum(P) sum(P),也就是sums中的正数部分)

因为 ( s u m ( n u m s ) + t a r g e t ) / 2 (sum(nums)+target)/2 (sum(nums)+target)/2是一个常数,设为new_target。所以求解目标就变为在数组中找到任意的数,组合起来之后求和等于new_target。这个过程可以考虑用动态规划来解,不然我感觉也没啥方法能解出来,暴力求解时间复杂度太高了。

dp[i]等于求和为i的组合的数量,例如dp[5]等于数组中能够组合起来求和为5的组合的数量。接下来就是要研究状态转移方程。

设当前在数组中遍历到的元素为cur,则dp[j]必然要加上dp[j-cur]。因为可以这么理解:当前元素为cur,那么j=j-cur+cur,所以组合求和为j的数量必然要加上组合求和为j-cur的数量。有多少种组合求和为j-cur,就需要加上多少种组合求和为j,因为当前元素为cur。为什么是加上呢?因为本来就有多种组合能满足求和为j,这个组合的数目就是dp[j]。所以状态转移方程:
d p [ j ] = d p [ j ] + d p [ j − c u r ] dp[j] = dp[j] + dp[j-cur] dp[j]=dp[j]+dp[jcur]
例如:
设 c u r = 3 , j = 9 , 则 : d p [ 9 ] = d p [ 9 ] + d p [ 9 − 3 ] d p [ 8 ] = d p [ 8 ] + d p [ 8 − 3 ] d p [ 7 ] = d p [ 7 ] + d p [ 7 − 3 ] … … … … … … d p [ 3 ] = d p [ 3 ] + d p [ 3 − 3 ] 设cur=3,j=9,则: \\ dp[9] = dp[9] + dp[9-3]\\ dp[8] = dp[8] + dp[8-3]\\ dp[7] = dp[7] + dp[7-3]\\ \dots\dots \\ \dots\dots \\ \dots\dots \\ dp[3] = dp[3]+dp[3-3] cur=3j=9dp[9]=dp[9]+dp[93]dp[8]=dp[8]+dp[83]dp[7]=dp[7]+dp[73]dp[3]=dp[3]+dp[33]
至于更新的顺序,应该是i=nums.size()--->i=cur,而不是i=cur--->i=nums.size()。为什么呢?其实可以自己算一算,如果从低到高的顺序来更新的话,大的结果出现冗余,会导致最终的结果大于正确的结果。因为更新的话,其实是用上一步的低dp加上当前的cur,才能更新高dp。如果从低到高来更新,则是利用这一步的低dp来更新高dp,那么必然存在冗余。

例如:设 n u m s = [ 1 ] nums=[1] nums=[1]。则初始状态 d p [ 0 ] = 1 , d p [ 1 ] = 0 , d p [ 2 ] = 0 dp[0]=1,dp[1]=0,dp[2]=0 dp[0]=1,dp[1]=0,dp[2]=0。接着遍历数组,cur=1。

如果从低至高更新:
d p [ 1 ] = d p [ 1 ] + d p [ 1 − 1 ] = 1 d p [ 2 ] = d p [ 2 ] + d p [ 2 − 1 ] = 1 ( 错 误 , 实 际 上 数 组 中 只 有 1 , 求 和 不 可 能 为 2 ) dp[1] = dp[1] + dp[1-1] = 1\\ dp[2] = dp[2] + dp[2-1] = 1\\ (错误,实际上数组中只有1,求和不可能为2) dp[1]=dp[1]+dp[11]=1dp[2]=dp[2]+dp[21]=112
如果从高至低更新:
d p [ 2 ] = d p [ 2 ] + d p [ 2 − 1 ] = 0 d p [ 1 ] = d p [ 1 ] + d p [ 1 − 1 ] = 1 ( 很 明 显 , 这 才 是 正 确 的 结 果 ) dp[2] = dp[2] + dp[2-1] = 0\\ dp[1] = dp[1] + dp[1-1] = 1\\ (很明显,这才是正确的结果) dp[2]=dp[2]+dp[21]=0dp[1]=dp[1]+dp[11]=1

提交代码

class Solution {
public:
    // 法一,直接暴力递归+回溯,尝试所有组合的值,效率较低
    // int findTargetSumWays(vector<int>& nums, int S) {
    //     if(nums.size() == 0)
    //         return 0;
    //     return dfs(nums, 0, (long long)S);
    // }
    // int dfs(vector<int>& nums, int cur_index, long long remain) {
    //     if (cur_index == nums.size())
    //         if (remain == 0)
    //             return 1;
    //         else 
    //             return 0;
    //     int cur = nums[cur_index];
    //     return dfs(nums, cur_index+1, remain-cur) + dfs(nums, cur_index+1, remain+cur);
    // }
    
    // 法二,动态规划进行优化
    int findTargetSumWays(vector<int>& nums, int S) {
        if(nums.size() == 0)
            return 0;
        int sum = 0;
        // 先对数组求和
        for (int i = 0; i < nums.size(); i++)
            sum += nums[i];
        int target = S;
        // 设P为nums中的正数,N为nums中的负数,sum(P)-sum(N)=sum(nums)
        // sum(P)+sum(N)=target
        // 两式相加,得到2*sum(P)=sum(nums)+target==>sum(p)=(sum(nums)+target)/2
        // 根据原式可知,如果sum+target不为偶数,或者sum<target,则必然无解
        // 接下来的目标就是在数组中找到任意的数,组合起来求和等于(sum(nums)+target)/2
        // 这个过程可以用动态规划来解。dp[i]即求和为i的方案数量
        // 比如dp[5]等于数组中能够组合起来求和为5的组合的数量
        // 状态转移方程就是dp[i] = dp[i] + dp[i-cur],cur为在数组中当前遍历的数
        if (sum < target || (sum+target) % 2 != 0)
            return 0;
        int new_target = (sum + target)/2;
        int dp[new_target+1] = {0};
        dp[0] = 1;
        for (int cur : nums) {
            for (int i = new_target; i >= cur; i--) {
                dp[i] += dp[i-cur];
            }
        }
        return dp[new_target];
    }
};

谢谢大家的阅读~~如果文中出现什么错误,欢迎指出,谢谢!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值