LeetCode494. 目标和

494. 目标和

文章目录

    • [494. 目标和](https://leetcode.cn/problems/target-sum/)
      • 一、题目
      • 二、题解
        • 方法一:目标和路径计数算法
        • 方法二:01背包
        • 方法三:01背包一维数组


一、题目

给你一个非负整数数组 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

二、题解

方法一:目标和路径计数算法

思考过程可以分为以下几步:

  1. 问题拆解: 首先,将问题拆解为更小的子问题。在这个问题中,我们可以将目标和问题拆解为在给定数组中选择一些数字,使得它们的和等于目标值。

  2. 状态定义: 确定动态规划的状态。在这个问题中,我们可以考虑使用二维数组 dp[i][j] 表示在前 i 个数字中,和为 j 的表达式的数量。

  3. 状态转移方程: 找到状态之间的转移关系。这是动态规划问题的核心。在这个问题中,对于每个数字 nums[i],我们有两种选择:添加正号或者添加负号。因此,状态转移方程可以表示为:dp[i][j] = dp[i-1][j-nums[i]] + dp[i-1][j+nums[i]],即前 i-1 个数字和为 j-nums[i] 的表达式数量加上前 i-1 个数字和为 j+nums[i] 的表达式数量,最后的数字是dp[i][j] = dp[i-1][abs(j-nums[i])] + dp[i-1][j+nums[i]],因为如果j < nums[i],我们是可以通过dp[i-1][nums[i]-j]得到dp[i][j]。举个例子,如图,为什么dp[2][0] = 2?图中已经说明白了。

LeetCode494. 目标和_第1张图片

  1. 边界条件: 根据问题的实际情况,确定边界条件。在这个问题中,我们可以从第一个数字开始考虑,初始化 dp[0][j],表示只有一个数字时,和为 j 的表达式数量。

  2. 问题求解: 根据状态转移方程和边界条件,从小问题逐步求解到原问题。填充二维数组 dp,最终 dp[nums.size()-1][abs(target)] 就是我们要的答案,表示在所有数字中,和为 target 的表达式数量。

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        // 定义一个二维数组 dp,大小为 (nums.size(), 2001),其中 dp[i][j] 表示在前 i 个数字中,和为 j 的表达式数量
        vector<vector<int>> dp(nums.size(), vector<int>(2001, 0));
        
        // 初始化:处理只有一个数字的情况
        for(int i = 0; i <= 1000; i++){
            if(nums[0] == i){
                if(nums[0] == 0) dp[0][i] = 2;  // 对于数字 0,可以有正号或负号
                else dp[0][i] = 1;
            }
        }
        
        // 填写 dp 数组
        for(int i = 1; i < nums.size(); i++){
            for(int j = 0; j <= 1000; j++){
                dp[i][j] = dp[i-1][abs(j-nums[i])] + dp[i-1][j+nums[i]];  // 根据状态转移方程计算 dp[i][j]
            }
        }
        
        return dp[nums.size()-1][abs(target)];  // 返回在所有数字中,和为 target 的表达式数量
    }
};

方法二:01背包

01背包问题通常是这样的:给定一组物品,每个物品有重量和价值,我们要选择一些物品放入一个容量有限的背包中,使得背包中物品的总重量不超过背包的容量,同时最大化物品的总价值。

在这道题中,我们可以将正数部分和负数部分(前面是加号的和前面是减号的)的数字分别看作两组物品。正数部分的数字相当于具有正的价值,负数部分的数字相当于具有负的价值。

具体步骤如下:

  1. 将数组 nums 拆分成两个子数组:addminusadd 包含所有正数部分的数字,minus 包含所有负数部分的数字。

    我们可以得到add-minus = targetadd+minus = sum,从而根据这两个式子得出(sum+target)/2 = add

  2. 使用01背包的思想:这个问题就转化成了所有数字凑成add这个数的方法。即背包容量是add,物品是nums数组里的所有元素。

  3. 使用动态规划来解决问题:我们创建一个二维数组 dp,其中 dp[i][j] 表示在考虑前 i 个物品时,总和等于 j 的方法数。

    • 初始化 dp[0][0] 为 1,因为当没有物品可选时,总和为 0 是唯一的一种方式;但如果nums[0] = 0,还有一种方式就是只取nums[0],则有两种方式。
    • 对于其它物品 nums[i],我们根据以下规则更新 dp[i][j]
      • 如果 j >= nums[i],那么我们可以选择是否将 nums[i] 放入背包,dp[i][j]等于不放入背包和放入背包方式的总和,即 dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i]]
      • 如果 j < nums[i],则nums[i]不可以放入背包dp[i][j] = dp[i-1][j]
  4. 最终的答案是 dp[nums.size()-1][add],表示在考虑所有物品后,总和等于 add 的方法数。

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int sum = 0;
        // 计算数组nums的总和
        for(int i = 0; i < nums.size(); i++){
            sum += nums[i];
        }
        // 如果总和与目标值之和为奇数,或者总和小于目标值的绝对值,则无解,返回0
        if((sum + target) % 2 == 1 || sum < abs(target)){
            return 0;
        }
        // 计算要构造的正数部分和,这是01背包问题中的背包容量
        int add = (sum + target) / 2;
        // 创建二维动态规划数组dp
        vector<vector<int>> dp(nums.size(),vector<int>(add+1, 0));
        
        // 初始化dp数组
        for(int i = 0; i <= add; i++){
            // 如果i等于第一个数字nums[0],表示可以用第一个数字构造和为i的方式有1种
            if(i == nums[0]){
                dp[0][i] = 1;
            }
        } 
        // 特殊情况处理
        if(nums[0] == 0) dp[0][0] = 2;
        // 普通情况:当没有物品可选时,总和为 0 是唯一的一种方式
        else dp[0][0] = 1;
        
        // 填写dp数组
        for(int i = 1; i < nums.size(); i++){
            for(int j = 0; j <= add; j++){
                // 如果当前数字nums[i]大于目标和j,无法用当前数字构造
                if(nums[i] > j) dp[i][j] = dp[i-1][j];
                // 选择是否将 `nums[i]` 放入背包,`dp[i][j]`等于不放入背包和放入背包方式的总和
                else dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i]];
            }
        }
        // 返回最终的结果,即在考虑所有数字后,构造和为add的方法数
        return dp[nums.size()-1][add];
    }
};

方法三:01背包一维数组

具体细节就是初始化这里,这个dp[0] = 1相当于是按照结论推初始化(因为按照二维的初始化方式是错误的),最好还是去理解一下二维的吧……

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

你可能感兴趣的:(LeetCode刷题,算法,LeetCode,动态规划)