回溯算法总结

参考: 代码随想录: 回溯算法理论基础

回溯三部曲

  1. 递归函数参数
  2. 递归终止条件
  3. 单层搜索逻辑

通用模版

void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}

组合问题

  1. leetcode 39 组合总和
    • 注意每次递推的起点, 需要使用 startIndex (i + 1)去避免重复的问题
  2. leetcode 40 组合总和 II
    • 涉及到去重的问题, candidates[i] === candidates[i - 1] 的情况则需要continue
    • 但是去重只需要树层去重, 而不需要树枝去重, 所以使用used来判断, 当used[i - 1] = false的时候证明是同一层级重复,则continue
    • 而used[i - 1] = true, 则证明是树枝层级下重复, 则不影响
if(i > 0 && candidates[i] === candidates[i - 1] && used[i - 1] === false) continue;
  1. leetcode 216 组合总和 III
    • 需要额外的 sum 变量,每次添加的元素的时候加上当前元素
    • 当 sum 大于 n 的时候直接返回, 因为后面都是无效的
    • 当 sum 等于 n 并且集合长度等于 k 时, 收集结果
  if(sum > n) return
  if(temp.length === k && sum === n){
    result.push([...temp])
    return
  }

切割问题

需要注意三个问题:

  • 如何模拟切割线
  • 如何终止
  • 如何截取子串
  1. leetcode 131 分割回文串
    • 终止条件: 当 index 等于给出字符串 s 的长度时,证明已经全部分割好, 可以收集结果
    • 单层的时候进行切割, 每次切割的结果进行验证, 符合则推入 temp, 不符合则 continue
  for(let i = index; i < s.length; i++){
    if(!check(s, index, i)) continue
    temp.push(s.slice(index, i + 1))
    backtracking(i + 1)
    temp.pop()
  }
  1. leetcode 93 复原 IP 地址
    • 终止条件: IP地址为四部分, 所以当 temp 长度等于4并且 index = s.length 也就是遍历完, 进行结果收集
    • 单层的时候进行切割, 每次切割的结果进行验证, 符合则推入 temp, 不符合则 continue
    • 验证: 长度大于1并且前导值为0, 长度大于3或者数值大于255时则break, 因为已经不符合IP
  for(let i = start; i < s.length; i++){
    let str = s.slice(start, i + 1)
    if(str.length > 3 || +str > 255) break;
    if(str.length > 1 && str[0] === "0") break;
    temp.push(str)
    backtracking(i + 1)
    temp.pop()
  }

子集问题

  1. leetcode 78 子集
    • 按照模版, 需注意 start 每次加1
  2. leetcode 90 子集 II
    • 去重 (同组合总和 II)
if(i > 0 && nums[i] === nums[i - 1] && used[i - 1] == false) continue

排列问题

  1. leetcode 46 全排列
    • 可以创建 used 来记录使用过的元素, 避免重复元素进栈
if(used[i] === 1) continue
temp.push(nums[i])
used[i] = 1
backtracking()
used[i] = 0
temp.pop()
  1. leetcode 47 全排列 II
    • 需要同一层级去重
    • 可以在同层级下创建一个 set 并且每次记录当前的元素, 如果元素已存在则 continue
let set = new Set()
for(let i = 0; i < nums.length; i++){
  if(used[i] === 1 || set.has(nums[i])) continue
  temp.push(nums[i])
  set.add(nums[i])
  used[i] = 1
  backtracking()
  used[i] = 0
  temp.pop()
}

其他问题

  1. leetcode 491 递增子序列
    • 验证后一个需要比前一个元素大, 也就是 num[i] > temp[temp.length - 1]
    • 使用used去重
const used = []
for(let i = start; i < nums.length; i++){
  if(temp.length > 0 && nums[i] < temp[temp.length - 1] || used[nums[i] + 100] === true) continue
  temp.push(nums[i])
  used[nums[i] + 100] = true
  backtracking(i + 1)
  temp.pop()
}
  1. leetcode 17 电话号码的组合
    • 终止条件: temp 和 digits 的长度一致则收取结果
    • 需要使用 startIndex, 并且每次递归查询 map 是否存在当前数字
if(index > digits.length || !map.has(digits[index])) return
let arr = map.get(digits[index])
for(let i = 0; i < arr.length; i++){
  temp.push(arr[i])
  backtracking(index + 1)
  temp.pop()
}

性能分析

子集问题分析:

  • 时间复杂度: O(2^n), 因为每一个元素的状态无外乎取与不取,所以时间复杂度为O(2^n)
  • 空间复杂度:O(n),递归深度为n,所以系统栈所用空间为O(n),每一层递归所用的空间都是常数级别,注意代码里的result和path都是全局变量,就算是放在参数里,传的也是引用,并不会新申请内存空间,最终空间复杂度为O(n)
  • 讲解
    • 数的每一层延伸的个数是 2 的次方, 所以选择的元素 N 就说明有 2 ^ N

排列问题分析:

  • 时间复杂度:O(n!),这个可以从排列的树形图中很明显发现,每一层节点为n,第二层每一个分支都延伸了n-1个分支,再往下又是n-2个分支,所以一直到叶子节点一共就是 n * n-1 * n-2 * … 1 = n!。
  • 空间复杂度:O(n),和子集问题同理。
  • 讲解
    • 第一层递归: 在第一层,我们有 N 个选择(nums 数组中的每个元素)。
    • 第二层递归: 在每个选择后,我们进入第二层,此时我们有 N−1 个选择。
    • 继续这个过程: 这个过程一直持续到我们达到深度 N 的递归,此时没有剩余元素可供选择。

组合问题分析:

  • 时间复杂度:O(2^n),组合问题其实就是一种子集的问题,所以组合问题最坏的情况,也不会超过子集问题的时间复杂度。
  • 空间复杂度:O(n),和子集问题同理。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值