菜单

回溯

相关源文件

回溯是一种系统的算法技术,通过逐步构建候选解并在确定其无效时放弃它们来找到计算问题的所有(或部分)解。本页面解释了回溯范式、其实现模式以及在编程面试中的常见应用。

有关相关算法技术的信息,请参阅动态规划贪心算法

什么是回溯?

回溯是一种算法范式,它使用深度优先搜索方法来探索问题的所有潜在解决方案。它逐步构建解决方案,在任何时候(“回溯”)丢弃不满足问题约束的解决方案并继续搜索。

来源:leetcode/backtracking/GenerateParentheses.java14-34 leetcode/backtracking/Permutations.java14-32

何时使用回溯

回溯在以下问题中特别有效:

  1. 您需要找到满足某些约束的所有(或部分)解决方案
  2. 解决方案可以逐步构建
  3. 每一步做出的决策会影响后续步骤的可用选项

常见问题类型包括

问题类型示例
组合问题排列、组合、子集
约束满足N皇后、数独
路径查找迷宫求解、骑士之旅
字符串生成生成有效括号

来源:leetcode/backtracking/GenerateParentheses.java1-11 leetcode/backtracking/Permutations.java1-12

回溯模板

大多数回溯解决方案遵循一个通用模式

通用伪代码模板是

function backtrack(currentState, ...):
    if (base case reached):
        add current solution to result set
        return
    
    for (each possible choice):
        if (choice is valid):
            make choice
            backtrack(updated state, ...)
            undo choice (backtrack)

来源:leetcode/backtracking/GenerateParentheses.java21-34

示例 1:生成括号

让我们来看一个经典的回溯问题:生成 n 对括号的所有有效组合。

问题陈述

给定 n 对括号,编写一个函数来生成所有格式正确的括号组合。

示例

对于 n = 3,输出应为

[
  "((()))",
  "(()())",
  "(())()",
  "()(())",
  "()()()"
]

实现

该解决方案涉及通过添加开括号和闭括号来递归构建字符串,同时保持有效性约束

  1. 如果我们还没有用完所有 n 个开括号,我们可以添加一个开括号
  2. 如果到目前为止开括号的数量多于闭括号,我们可以添加一个闭括号

仓库中的实现使用了一个名为 generateParenthesisRecursive 的函数,该函数跟踪

  • 当前正在构建的字符串
  • 已使用的开括号数量
  • 已使用的闭括号数量
  • 所需的总对数

来源:leetcode/backtracking/GenerateParentheses.java14-34 company/google/GenerateParentheses.java14-34

示例 2:排列

另一个常见的回溯问题是生成一组元素的所有排列。

问题陈述

给定一组不同的数字,返回所有可能的排列。

示例

对于输入 [1,2,3],输出应为

[
  [1,2,3],
  [1,3,2],
  [2,1,3],
  [2,3,1],
  [3,1,2],
  [3,2,1]
]

实现

虽然经典的回溯方法会跟踪已使用的元素并递归构建排列,但仓库中的实现采用了一种迭代方法

这种方法从一个空排列开始,通过将每个数字插入到现有部分排列的所有可能位置来迭代地构建所有排列。

来源:leetcode/backtracking/Permutations.java14-32 company/microsoft/Permutations.java14-32 company/linkedin/Permutations.java14-32

回溯 vs. 动态规划

了解何时使用回溯与动态规划非常重要

功能回溯动态规划
目的找到所有(或部分)解决方案找到最优解
方法探索所有可能的路径(可能会丢弃一些)从重叠子问题构建解决方案
时间复杂度通常是指数级的通常是多项式级的
问题类型决策问题,枚举优化问题
示例生成所有有效括号找到网格中的最短路径

UniquePaths 问题展示了动态规划比回溯更有效的一个案例

来源:leetcode/dynamic-programming/UniquePaths.java8-31

时间和空间复杂度分析

回溯算法通常具有指数时间复杂度,因为它们探索许多可能的组合

问题时间复杂度空间复杂度
生成括号O(4^n / √n)O(n)
排列O(n!)O(n!)

空间复杂度包括

  • 递归栈深度(通常为 O(n))
  • 所有有效解决方案的存储

来源:leetcode/backtracking/GenerateParentheses.java14-34 leetcode/backtracking/Permutations.java14-32

回溯解决方案的最佳实践

  1. 清晰地定义您的状态:您需要跟踪哪些信息才能在每个步骤中做出决策?

  2. 识别约束:是什么使部分解决方案有效或无效?

  3. 通过剪枝进行优化:尽早放弃无效路径以减少探索。

  4. 避免重复工作:适当时考虑使用记忆化。

  5. 考虑迭代实现:在某些情况下,迭代方法可能更高效或更清晰,如排列示例所示。

在实现回溯解决方案时,请着重于

  • 正确定义基本情况
  • 在递归过程中正确管理状态
  • 理解问题的分支因子

来源:leetcode/backtracking/GenerateParentheses.java21-34 leetcode/backtracking/Permutations.java14-32