前端刷题路-Day58:目标和(题号494)

509 阅读3分钟

这是我参与更文挑战的第22天,活动详情查看: 更文挑战

目标和(题号494)

题目

给你一个整数数组 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 <= 100

链接

leetcode-cn.com/problems/ta…

解释

这题啊,这题是经典动态规划。

首先看看暴力解法,暴力解法就很简单了,直接一个递归找到所有可能性就完事了,这没啥可说的。

重点是动态规划的思路,笔者是在回家的路上思考的,思考了一路也没有什么好的想法,即使这题就差把动态规划四个字写在题目上了。

看来答案才知道原来这题是得通过一些计算才能动态规划的,笔者的DP一直卡在符号上面,有了符号就不太方便DP了,因为公式一直想不出来。

那要怎样才能把符号去掉呢?这就需要一些计算了。

这里直接引用官方的解释了,笔者也就不二次翻译了👇:

记数组的元素和为sum,添加 - 号的元素之和为 neg,则其余添加 + 的元素之和为 sum−neg,得到的表达式的结果为

(sumneg)neg=sum2neg=target(sum−neg)−neg=sum−2⋅neg=target

neg=sumtarget2neg= \frac{sum−target}{2}

由于数组 nums 中的元素都是非负整数,neg 也必须是非负整数,所以上式成立的前提是 sum−target 是非负偶数。若不符合该条件可直接返回 0

若上式成立,问题转化成在数组 nums 中选取若干元素,使得这些元素之和等于 neg,计算选取元素的方案数。我们可以使用动态规划的方法求解。

之后的过程就很简单了,利用动态规划找到dp[i][j],当jneg时,就是我们想要的答案了。

自己的答案

更好的方法(暴力)

暴力法很简单,👇:

var findTargetSumWays = function(nums, target) {
  var count = 0
  function DFS(sum, index) {
    if (index === nums.length) {
      if (sum === target) {
        count++
      }
    } else {
      DFS(sum + nums[index], index + 1)
      DFS(sum - nums[index], index + 1)
    }
  }
  DFS(0, 0)
  return count
};

搞一个深度优先搜索的函数,然后递归调用就完事了,没有什么多余的技巧,在每次到结尾的时候判断sum是否等于target,很简单。

更好的方法(动态规划)

DP这里稍微有点复杂,在开始DP前,有两步准备工作,首先,拿到所有的数字和,之后找到negdiff进行剪枝。

之后根据neg进行DP操作,找到所有的数字和为neg的可能性,就是这题的答案。

var findTargetSumWays = function(nums, target) {
  var sum = 0
      diff = 0
      neg = 0
      len = nums.length
      dp = null
  for (const num of nums) {
     sum += num
  }
  diff = sum - target
  // 剪枝
  if (diff < 0 || diff % 2 !== 0) return 0
  neg = diff / 2
  dp = Array.from({length: len + 1}, () => new Array(neg + 1).fill(0))
  dp[0][0] = 1
  for (let i = 1; i <= len; i++) {
    var num = nums[i - 1]
    for (let 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[len][neg]
};

剪枝的过程就是这行代码👇:

if (diff < 0 || diff % 2 !== 0) return 0

第一种情况,由于所有的数字都为非负整数,那么就是说如果diff小于0,就代表着target是大于所有数字和的,这种情况下显然不可能存在任何可能性。

第二种情况,由于在解释中得到了一个等式neg = diff / 2,如果diff无法被二整除,那就意味着neg是个小数,由于所有的数字都是整数,显然不可能组成小数,所以这种情况下也是没有任何可能的。

剪枝完成后就是经典DP了,这里不多赘述,如果看不懂的就多看几次,这种程度的DP还是不难理解的。

更好的方法(动态规划->降维)

又是经典的动态规划降维操作,因为每次新的DP结果只和上一次的结果有关,显然只需要存上一次结果就好。

但这里需要注意,由于需要保证dp[i-1][]中的值,所有内层循环需要进行倒序操作👇:

var findTargetSumWays = function(nums, target) {
    let sum = 0;
    for (const num of nums) {
        sum += num;
    }
    const diff = sum - target;
    if (diff < 0 || diff % 2 !== 0) {
        return 0;
    }
    const neg = diff / 2;
    const dp = new Array(neg + 1).fill(0);
    dp[0] = 1;
    for (const num of nums) {
        for (let j = neg; j >= num; j--) {
            dp[j] += dp[j - num];
        }
    }
    return dp[neg];
};


PS:想查看往期文章和题目可以点击下面的链接:

这里是按照日期分类的👇

前端刷题路-目录(日期分类)

经过有些朋友的提醒,感觉也应该按照题型分类
这里是按照题型分类的👇

前端刷题路-目录(题型分类)

有兴趣的也可以看看我的个人主页👇

Here is RZ