回溯

递归 + 单重循环 · 树形探索

回溯模板总结

回溯 vs 双重循环的区别
  • 回溯 = 递归 + 单重循环,和双重循环不一样
  • 回溯:for 循环的每个选择都会递归进入下一层,形成树形搜索
  • 双重循环:固定两层嵌套,所有循环在同一层次
  • 时间复杂度通常是指数级或阶乘级(O(2^n) 或 O(n!))
// 回溯通用模板
function backtrack(路径, 选择列表) {
  if (满足结束条件) {
    result.push(路径.slice()) // 注意要拷贝
    return
  }
  for (let 选择 of 选择列表) {
    做选择      // stack.push / remain -= val
    backtrack(路径, 选择列表)
    撤销选择    // stack.pop / remain += val
  }
}

17 电话号码的字母组合

回溯入门模板题

题干:给定数字字符串(2-9),返回所有可能的字母组合。如 "23"["ad","ae","af","bd",...]

var letterCombinations = function (digits) {
  if (!digits) return []
  const map = new Map([
    [2, 'abc'], [3, 'def'], [4, 'ghi'], [5, 'jkl'],
    [6, 'mno'], [7, 'pqrs'], [8, 'tuv'], [9, 'wxyz'],
  ])
  let stack = []
  let ans = []

  const helper = (curIndex) => {
    if (curIndex === digits.length) {
      ans.push(stack.slice().join('')) // 收集结果
      return
    }
    let charArr = map.get(parseInt(digits[curIndex]))
    for (let i = 0; i < charArr.length; i++) {
      stack.push(charArr[i])   // 做选择
      helper(curIndex + 1)     // 递归下一层
      stack.pop()              // 撤销选择
    }
  }
  helper(0)
  return ans
}

22 括号生成

两种选择:加左括号 or 加右括号

题干:给定 n 对括号,生成所有合法的括号组合。

  • 可加左括号条件:lRemain > 0
  • 可加右括号条件:rRemain > lRemain(已用的左括号比右括号多)
var generateParenthesis = function(n) {
  let lRemain = n, rRemain = n
  let stack = []
  let ans = []

  const helper = (index) => {
    if (index === 2 * n) {
      ans.push(stack.slice().join(''))
      return
    }
    // 可以加左括号
    if (lRemain > 0) {
      stack.push('(')
      lRemain--
      helper(index + 1)
      stack.pop()
      lRemain++
    }
    // 可以加右括号(已用左括号比右括号多)
    if (rRemain > lRemain) {
      stack.push(')')
      rRemain--
      helper(index + 1)
      stack.pop()
      rRemain++
    }
  }
  helper(0)
  return ans
}

39 组合总和

排序 + 回溯 + beginIndex 去重

题干:从候选数组中找出所有和为 target 的组合,数字可以重复使用

  • 先排序,方便剪枝
  • beginIndex 避免重复组合(只往后取,不回头)
  • 递归传 i(而不是 i+1),允许重复选同一个数
var combinationSum = function(candidates, target) {
  candidates.sort((a, b) => a - b) // 排序方便剪枝
  let ansArr = []
  let curArr = []

  const helper = (remain, beginIndex) => {
    if (remain === 0) {
      ansArr.push(curArr.slice()) // 收集结果(注意拷贝)
      return
    }
    for (let i = beginIndex; i < candidates.length; i++) {
      let item = candidates[i]
      if (remain >= item) {
        curArr.push(item)      // 做选择
        remain -= item
        helper(remain, i)      // i 不是 i+1,允许重复
        remain += item         // 撤销选择
        curArr.pop()
      }
    }
  }
  helper(target, 0)
  return ansArr
}