力扣-494. 目标和

1.题目

题目地址(494. 目标和 - 力扣(LeetCode))

https://leetcode.cn/problems/target-sum/

题目描述

给你一个非负整数数组 nums 和一个整数 target

向数组中的每个整数前添加 '+''-' ,然后串联起所有整数,可以构造一个 表达式

  • 例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1"

返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。

 

示例 1:

输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 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
+1 + 1 + 1 + 1 - 1 = 3

示例 2:

输入:nums = [1], target = 1
输出:1

 

提示:

  • 1 <= nums.length <= 20
  • 0 <= nums[i] <= 1000
  • 0 <= sum(nums[i]) <= 1000
  • -1000 <= target <= 1000

2.题解

这里每个数仅能选择一次,是一个0/1背包问题

2.1 数学+动态规划(一维dp数组)

思路

首先我们如果思考想将这一题转换为一个动态规划问题,或者说一个0-1背包问题
我们必须明确选/不选什么类型的元素,这里有要加的元素/要减的元素两种,那么我们有必要设计一个二维dp数组,分别来记录这两种元素吗?
答案是没有必要的,由于+元素/-元素和target之间必然存在某种数学关系,我们可以通过这种数学关系来简化计算

首先我们假设sum = 所有元素的和(当作unsigned,无论符号),那么我们假设-元素和(这里的和为单纯的元素和)为neg, +元素便为sum - neg
target = +元素 - (-元素)= sum - neg - neg = sum - 2*neg
neg = (sum - target) / 2
其中sum很好求,我们简化使用accumulate函数,注意一共三个参数:数组的首位迭代器和初始值

接下来我们便可以利用动态规划来求解能组成元素和为neg的组合了
dp[i] = (在[0,i]个元素中任意选择元素,和为...), 如果我们使用这种传统思维,便会发现其中的局限性,如何在和为茫茫dp数组中寻找和为neg的那个组合?
我们逆转思路,dp[i]=(表示和为i的组合个数),且每一次新的组合都可以是基于前面已经列出组合 + 一个新元素 = 组成的新组合, 也就是有了递推关系
且最后我们寻找和为neg的组合个数时,直接求dp[neg]即可

之前我们的 查找条件(在索引[0,i]之间寻找,), 查找结果(根据题目而定)
这里还是遵循 查找条件(和为neg), 查找结果(组合个数)

代码1(使用二维dp)

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int sum = 0;
        for (int& num : nums) {
            sum += num;
        }
        int diff = sum - target;
        if (diff < 0 || diff % 2 != 0) {
            return 0;
        }
        int n = nums.size(), neg = diff / 2;
        vector<vector<int>> dp(n + 1, vector<int>(neg + 1));
        dp[0][0] = 1;
        for (int i = 1; i <= n; i++) {
            int num = nums[i - 1];
            for (int j = 0; j <= neg; j++) {
                dp[i][j] = dp[i - 1][j];
                if (j >= num) {
                    dp[i][j] += dp[i - 1][j - num];
                }
            }
        }
        return dp[n][neg];
    }
};

代码2(简化,使用一维dp)

  • 语言支持:C++
    C++ Code:
class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int n = nums.size();
        int sum = accumulate(nums.begin(), nums.end(), 0);
        if(sum - target < 0 || (sum - target) & 1) return 0;
        int neg = (sum - target) / 2;
        vector<int> dp(neg + 1);
        dp[0] = 1; // 什么都不选是一种选择方式
        for(int i = 0; i < n; i++){
            int num = nums[i];
            // 为了避免覆盖,使用倒序更新
            for(int j = neg; j >= num; j--){
                dp[j] += dp[j - num];
            }
        }
        return dp[neg];
    }
};

复杂度分析

  • 时间复杂度:\(O(n\times(sum-target))\),其中\(n\)是数组nums的长度,sum是数组nums的元素和,target是目标数。动态规划有\((n+1)\times(\frac{sum-target}2+1)\)个状态,需要计算每个状态的值。

  • 空间复杂度:\(O(sum-target)\),其中sum是数组nums的元素和,target是目标数。使用空间优化的实现,需要创建长度为\(\frac{sum-target}2+1\)的数组\(dp\)

2.2 回溯

思路

要么加,要么减,我们可以考虑利用回溯遍历所有可能性,在一种可能性结束后,进行回溯,执行另一种可能性。

代码

class Solution {
public:
    int count = 0;
    int findTargetSumWays(vector<int>& nums, int target) {
        backtrack(nums, target, 0, 0);
        return count;
    }

    void backtrack(vector<int> &nums, int target, int index, int sum){
        if(index == nums.size()){
            if(sum == target){
                count++;
            }
        }else{
            backtrack(nums, target, index + 1, sum + nums[index]);
            backtrack(nums, target, index + 1, sum - nums[index]);
        }
    }
};

复杂度分析

  • 时间复杂度:\(O(2^n)\),其中\(n\)是数组nums的长度。回溯需要遍历所有不同的表达式,共有\(2^n\)种不同的表达式,每种表达式计算结果需要\(O(1)\)的时间,因此总时间复杂度是\(O(2^n)\)

  • 空间复杂度:\(O(n)\),其中\(n\)是数组nums的长度。空间复杂度主要取决于递归调用的栈空间,栈的深度不超过\(n\)

posted @ 2024-06-04 10:22  DawnTraveler  阅读(9)  评论(0编辑  收藏  举报