LeetCode-Py回溯算法框架:子集/排列/组合问题的通用解法
回溯算法(Backtracking)是一种系统地搜索所有可能解的算法,通过递归和试错的方式逐步构建解的过程。当发现当前路径无法满足题目要求或无法得到有效解时,算法会撤销上一步的选择(即「回溯」),返回到上一个决策点,尝试其他可能的路径。回溯法的核心思想是「走不通就退回,换条路再试」,而每次需要回退的节点称为「回溯点」。详细理论基础可参考docs/07_algorithm/07_04_backtracking_algorithm.md。
回溯算法通用框架
回溯算法的核心流程可概括为「选择-递归-回溯」三步,其通用模板如下:
res = [] # 存放所有符合条件结果的集合
path = [] # 存放当前递归路径下的结果
def backtracking(参数):
if 满足结束条件: # 例如:路径长度达到目标或遍历完所有元素
res.append(path[:]) # 拷贝当前路径到结果集
return
for 选择 in 可选列表:
if 剪枝条件: # 例如:元素已使用或不符合约束
continue
path.append(选择) # 做出选择
backtracking(新参数) # 递归探索下一层
path.pop() # 撤销选择,回溯到上一步
该框架在docs/07_algorithm/07_04_backtracking_algorithm.md中有完整实现,通过调整参数和剪枝条件可解决各类组合优化问题。
子集问题解法
子集问题要求找出数组中所有可能的不重复子集,如输入[1,2,3]需返回[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]。这类问题的关键是避免重复选择,可通过索引控制实现。
算法实现
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
res = [] # 存储所有子集结果
path = [] # 存储当前子集路径
def backtracking(index: int):
res.append(path[:]) # 记录当前路径(包含空集)
if index >= len(nums):
return
for i in range(index, len(nums)):
path.append(nums[i]) # 选择当前元素
backtracking(i + 1) # 递归选择下一个元素
path.pop() # 撤销选择
backtracking(0)
return res
核心要点
- 索引控制去重:通过
index参数确保每次递归只选择后续元素,避免[1,2]和[2,1]这样的重复子集 - 结果收集时机:进入回溯函数即收集当前路径,确保空集和所有中间状态都被记录
- 剪枝优化:无需额外剪枝,索引机制已天然避免重复
完整解题思路可参考docs/07_algorithm/07_04_backtracking_algorithm.md中的子集问题分析。
排列问题解法
排列问题与子集问题的主要区别在于元素顺序,如[1,2]和[2,1]是不同排列。因此需要额外记录已使用元素,防止重复选择。
算法实现
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
res = [] # 存储所有排列结果
path = [] # 存储当前排列路径
def backtracking():
if len(path) == len(nums):
res.append(path[:])
return
for i in range(len(nums)):
if nums[i] in path: # 剪枝:跳过已使用元素
continue
path.append(nums[i]) # 选择当前元素
backtracking() # 递归选择下一个元素
path.pop() # 撤销选择
backtracking()
return res
核心要点
- 使用标记去重:通过
nums[i] in path判断元素是否已使用,也可使用布尔数组提高效率 - 结果收集时机:仅当路径长度等于数组长度时才收集结果
- 决策树遍历:每个节点需要尝试所有未使用元素,生成全排列
全排列问题的决策树结构可参考docs/07_algorithm/07_04_backtracking_algorithm.md中的图示说明。
组合问题解法
组合问题通常要求从数组中选取特定数量的元素,如「从[1,2,3,4]中选2个元素的所有组合」。这类问题需要结合数量限制和索引控制。
算法实现
class Solution:
def combine(self, n: int, k: int) -> List[List[int]]:
res = [] # 存储所有组合结果
path = [] # 存储当前组合路径
def backtracking(start: int):
if len(path) == k:
res.append(path[:])
return
# 剪枝:剩余元素不足时停止
for i in range(start, n - (k - len(path)) + 2):
path.append(i) # 选择当前数字
backtracking(i + 1) # 递归选择下一个数字
path.pop() # 撤销选择
backtracking(1)
return res
核心要点
-
双重剪枝优化:
- 索引控制:
start参数确保元素递增选择 - 数量控制:
n - (k - len(path)) + 2确保剩余元素足够组成k个元素
- 索引控制:
-
终止条件:当路径长度等于k时收集结果
组合问题的更多变种可参考docs/solutions/0001-0099/combination-sum.md中的组合总和问题。
三类问题对比与优化
| 问题类型 | 核心特点 | 去重方式 | 结果收集时机 | 典型题目 |
|---|---|---|---|---|
| 子集 | 元素无序,不重复选取 | 索引控制 | 进入回溯即收集 | 78. 子集 |
| 排列 | 元素有序,不重复选取 | 标记数组/集合 | 路径长度达标时 | 46. 全排列 |
| 组合 | 元素无序,固定数量 | 索引控制+数量剪枝 | 路径长度达标时 | 39. 组合总和 |
通用优化技巧
-
剪枝策略:
- 提前终止:当剩余元素不足时停止递归
- 排序去重:对数组排序后跳过相同元素(如含重复元素的子集问题)
- 条件过滤:根据问题约束提前排除无效路径
-
空间优化:
- 使用全局变量代替参数传递
- 结果集使用引用传递而非值传递
-
时间优化:
- 使用布尔数组代替
in操作判断元素是否使用 - 预计算边界条件减少循环次数
- 使用布尔数组代替
实战应用与练习
回溯算法在LeetCode中有大量应用,以下是推荐练习的典型题目:
完整题目列表可参考docs/00_preface/00_06_categories_list.md#回溯算法题目。
总结
回溯算法是解决排列、组合、子集等枚举类问题的通用方法,其核心在于通过「选择-递归-回溯」的流程遍历决策树。掌握以下关键点可有效提升解题能力:
- 问题建模:将实际问题转化为决策树结构
- 状态表示:设计合适的路径和结果集存储方式
- 剪枝优化:根据问题特点设计高效剪枝条件
- 去重策略:灵活运用索引控制、标记数组等方式避免重复解
通过本文介绍的通用框架和三类问题的具体实现,可应对大多数回溯算法题目。建议结合推荐练习题目深入理解不同场景下的变形应用,逐步掌握回溯算法的精髓。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



