目标和
https://leetcode-cn.com/problems/target-sum/
https://labuladong.gitee.io/algo/3/24/75/
回溯法
很容易想到最简单的回溯法,就是在每一个位置决定是加法还是减法。
其中变量 track 表示当前回溯路径(当前已经积累的和)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
   | var res int
  func findTargetSumWays(nums []int, target int) int {     res = 0     backtrack(nums, 0, 0, target)     return res }
  func backtrack(nums []int, i, track, target int) {     n := len(nums)     if i == n {         if track == target {             res++         }         return     }
      backtrack(nums, i + 1, track - nums[i], target)     backtrack(nums, i + 1, track + nums[i], target)     return }
 
  | 
 
备忘录优化
简单回溯,是具有重复子问题的。是否存在重复子问题,要关注递归函数中会变的参数 i 和 track。看这一段:
1 2
   | backtrack(nums, i + 1, track - nums[i], target) backtrack(nums, i + 1, track + nums[i], target)
 
  | 
 
当 nums[i] 为 0 时,显然是两个重复的函数调用。因此用备忘录记录会变的参数对 i, track,速度明显提升。
如果使用备忘录,需要让函数返回一个值。这里当回溯到终点且达到目标是,返回一个 1,表面这是一种可行的解法,否则返回 0。递归时将子问题加起来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
   | var memo map[[2]int]int
 
  func findTargetSumWays(nums []int, target int) int {     memo = make(map[[2]int]int)     return backtrack(nums, 0, 0, target) }
  func backtrack(nums []int, i, track, target int) int {     n := len(nums)     if i == n {         if track == target {             return 1         }         return 0     }
      if v, ok := memo[[2]int{i, track}]; ok {         return v     }
      res := backtrack(nums, i + 1, track - nums[i], target) + backtrack(nums, i + 1, track + nums[i], target)     memo[[2]int{i, track}] = res     return res }
 
  | 
 
动态规划
此问题可以转换成背包问题,类似 分割等和子集,然后用动态规划来解决。
首先,如果我们把 nums 划分成两个子集 A 和 B,分别代表分配 + 的数和分配 - 的数,那么他们和 target 存在如下关系:
1 2 3 4 5
   | sum(A) - sum(B) = target =>  sum(A) = target + sum(B) =>  sum(A) + sum(A) = target + sum(B) + sum(A) =>  2 * sum(A) = target + sum(nums) =>  sum(A) = (target + sum(nums)) / 2
 
  | 
 
即:需要在 nums 中找到一个子集 A ,使得它的和为 (target + sum(nums)) / 2。需要求一共有多少个这样的子集。
定义 dp[i][j] 为前 i 个物品,当前背包容量为 j,有 dp[i][j] 种凑法使得子集的和为 (target + sum(nums)) / 2。
base case: dp[0][0] 为 1,因为当只有 0 个物品且背包容量为 0,什么都不干就是一个这样的子集。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
   | func findTargetSumWays(nums []int, target int) int {     sum := 0     for _, n := range nums {         sum += n     }
      if sum < abs(target) || (target + sum) % 2 != 0 {         return 0     }     vol := (target + sum) / 2
      return subsets(nums, vol) }
  func subsets(nums []int, target int) int {     n := len(nums)
      dp := make([][]int, n+1)     for i := range dp {         dp[i] = make([]int, target+1)     }
      dp[0][0] = 1
      for i := 1; i <= n; i++ {         for j := 0; j <= target; j++ {             if j - nums[i-1] >= 0 {                 dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i-1]]             } else {                 dp[i][j] = dp[i-1][j]             }         }     }
      return dp[n][target] }
  func abs(a int) int {     if a < 0 {         return -a     }     return a }
 
  |