Leetcode基础算法-回溯算法


1. 回溯算法简介

回溯算法(Backtracking)是一种避免不必要搜索的穷举式搜索算法. 它采用试错的思想, 在搜索过程中, 当某一步选择无法满足求解条件时, 退回一步(回溯)重新选择. 简单来说, 回溯算法的核心思想是「走不通就回退」. 它通常通过递归实现.

回溯算法的两种典型结果:

  • 找到一个可能的解.
  • 尝试了所有可能的方法后, 宣布无解.

全排列问题示例
[1, 2, 3] 为例, 使用回溯法来生成全排列:

  • 选择 1 为开头, 得到 [1, X, X]

    • 然后选择2为中间数,得到[1, 2, X]
      • 最后一位只能选择3, 得到[1, 2 ,3]
      • 回溯
    • 然后选择3为中间数,得到[1, 3, X]
      • 最后一位只能选择2, 得到[1, 3 ,2]
      • 回溯
    • 回溯
  • 选择 2 为开头, 得到 [2, X, X]

    • 然后选择1为中间数,得到[2, 1, X]
      • 最后一位只能选择3, 得到[2, 1 ,3]
      • 回溯
    • 然后选择3为中间数,得到[2, 3, X]
      • 最后一位只能选择1, 得到[2, 3 ,1]
      • 回溯
    • 回溯
  • 选择 3 为开头, 得到 [3, X, X]

    • 然后选择1为中间数,得到[3, 1, X]
      • 最后一位只能选择2, 得到[3, 1 ,2]
      • 回溯
    • 然后选择2为中间数,得到[3, 2, X]
      • 最后一位只能选择1, 得到[3, 2 ,1]
      • 回溯
    • 回溯
  • 遍历所有可能的排列
    使用回溯法生成全排列的树状图如下所示:

[ ] (初始状态)
├── 1
│   ├── 2
│   │   └── 3 -> [1, 2, 3]
│   └── 3
│       └── 2 -> [1, 3, 2]
├── 2
│   ├── 1
│   │   └── 3 -> [2, 1, 3]
│   └── 3
│       └── 1 -> [2, 3, 1]
└── 3
    ├── 1
    │   └── 2 -> [3, 1, 2]
    └── 2
        └── 1 -> [3, 2, 1]
  • 每个分支表示选择了一个数字。
  • 每当路径中的数字数量达到 3(也就是 nums 列表的长度),就表示找到了一个完整的排列(例如 [1, 2, 3])。
  • 完整的排列标记在每个路径的末端,路径构造完成后回溯到上一个选择点。

回溯算法核心步骤

  • 选择元素:从未被选中的元素中选择.
  • 递归搜索:递归进入下一层, 直到满足边界条件.
  • 撤销选择:回退选择, 尝试新的分支.

2. 回溯算法的代码实现

以全排列问题为例, 回溯算法代码如下:

class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:
        res = []    # 存放所有符合条件结果的集合
        path = []   # 存放当前符合条件的结果
        
        def backtracking(nums):
            if len(path) == len(nums):  # 当path长度等于nums长度时,找到一个完整排列
                res.append(path[:])  # 将当前结果复制一份,放入结果集res中
                return

            for i in range(len(nums)):  # 遍历nums中的每一个元素
                if nums[i] not in path:  # 检查该元素是否已在path中
                    path.append(nums[i])  # 将当前元素加入path
                    backtracking(nums)    # 递归调用,继续构建下一个数字
                    path.pop()            # 撤销选择,移除最后一个元素,进行下一轮选择

        backtracking(nums)
        return res

工作流程详解

初始状态

  • nums = [1, 2, 3]
  • res = [] (最终结果存放全排列)
  • path = [] (当前路径,存放正在构建的排列)

递归执行流程

  1. 第一次递归:选择第一个元素

    • for i in range(len(nums)): 第一次循环 i = 0,选择 nums[0] = 1
    • path.append(1)path = [1]
    • 递归调用 backtracking(nums),此时 path = [1]
  2. 第二次递归:选择第二个元素

    • for i in range(len(nums)): 由于 1 已经在 path 中,跳过 nums[0],选择 nums[1] = 2
    • path.append(2)path = [1, 2]
    • 递归调用 backtracking(nums),此时 path = [1, 2]
  3. 第三次递归:选择第三个元素

    • for i in range(len(nums)): 跳过 nums[0]nums[1],选择 nums[2] = 3
    • path.append(3)path = [1, 2, 3]
    • 递归调用 backtracking(nums),此时 path = [1, 2, 3]
  4. 找到一个完整的排列

    • 现在 path = [1, 2, 3],长度等于 nums 长度,符合全排列条件。
    • path 的副本加入 res,即 res = [[1, 2, 3]]
    • 回溯path.pop() 撤销选择,path = [1, 2],返回上一层递归。
  5. 回溯:尝试其他可能性

    • 回到第二次递归中的 path = [1, 2],继续 for i in range(len(nums)) 的循环。
    • 当前已经选择过 nums[2],因此回溯到 path = [1],继续寻找。
  6. 继续递归:选择其他分支

    • 选择 nums[2] = 3 作为第二个元素:
      • path = [1, 3],递归后选择 nums[1] = 2
      • path = [1, 3, 2],找到第二个排列,将其加入 resres = [[1, 2, 3], [1, 3, 2]]
  7. 回溯到更上层,选择其他分支

    • 回溯到 path = [],选择 nums[1] = 2 作为第一个元素。
    • 继续递归,分别选择 nums[0] = 1nums[2] = 3
    • 最终找到 path = [2, 1, 3]path = [2, 3, 1],将其加入 resres = [[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1]]
  8. 最后的回溯和分支

    • 回溯到 path = [],选择 nums[2] = 3 作为第一个元素。
    • 继续递归选择 nums[0] = 1nums[1] = 2,找到 path = [3, 1, 2]path = [3, 2, 1]
    • 将它们加入 res,最终 res = [[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]

总结

  • 递归构建:在每次递归中,函数会尝试从 nums 中选择一个未被使用的元素,并将其添加到 path 中。当 path 的长度等于 nums 的长度时,说明找到了一个完整的排列。
  • 回溯撤销选择:一旦找到一个排列,或者尝试完所有可能性,函数会回溯,撤销之前的选择,然后尝试新的选择,直到遍历所有可能的排列。
  • 最终结果:通过回溯,所有排列都会被找到并存储在 res 中,最后返回。

关键点

  • 递归:每一层递归负责选择当前的位置的元素。
  • 回溯:当递归完成或需要更换路径时,撤销最后一个选择,回到上一层继续探索。
  • 去重:通过 if nums[i] not in path 这一条件来确保每个排列中的元素不会重复使用。

这样,程序能够正确生成 [1, 2, 3] 的所有排列。

3. 回溯算法通用模板

以下为回溯算法的一般模板:

res = []    # 存放所有符合条件结果的集合
path = []   # 存放当前符合条件的结果

def backtracking(nums):
    if 遇到边界条件:  # 说明找到了一组符合条件的结果
        res.append(path[:])  # 将结果加入集合
        return

    for i in range(len(nums)):  # 枚举可选元素列表
        path.append(nums[i])  # 选择元素
        backtracking(nums)    # 递归搜索
        path.pop()            # 撤销选择

4. 回溯算法三步走

为了简化回溯算法的理解, 可以将其归纳为三步:

  1. 明确所有选择:根据决策树, 列出所有可选路径.
  2. 明确终止条件:找到递归的边界条件, 如达到叶子节点.
  3. 将决策树和终止条件翻译成代码
    • 定义回溯函数, 传入参数与全局变量.
    • 编写回溯主体部分:约束条件、递归搜索、撤销选择.
    • 明确递归终止条件, 处理终止时的结果.

5. 小结

回溯算法本质上是深度优先搜索(DFS), 当遇到不符合条件的节点时, 回退并探索其他可能的路径. 回溯的核心是递归和状态重置(撤销选择), 常用于解决排列组合、子集问题等. 通过掌握回溯算法的模板和决策树的思路, 可以应对各种复杂的搜索问题.

6. 补充内容:

  • 回溯算法核心思想是通过递归解决问题, 熟练掌握递归和回溯状态的转换尤为重要.
  • 在编写回溯代码时, 关键是理清递归的结束条件和每步的选择操作.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值