回溯算法解题框架是什么?

2025年 阅读约 15 分钟 面试指南 · 算法

深入解析回溯算法解题框架:回溯三部曲(选择、递归、撤销)、全排列(含重复元素)、子集、组合总和、N皇后,剪枝优化技巧,附面试模拟问答。

一句话总结

回溯算法本质是多叉树的深度优先遍历(DFS),核心是回溯三部曲1)做选择(将当前元素加入路径)→ 2)递归(进入下一层决策树)→ 3)撤销选择(回溯,恢复状态)。模板:for 选择 in 选择列表: 做选择; backtrack(路径, 选择列表); 撤销选择。排列问题用 used 数组去重,组合/子集用 start 索引去重。

初级理解

回溯算法模板

def backtrack(路径, 选择列表): if 满足结束条件: result.append(路径[:]) # 注意拷贝! return for 选择 in 选择列表: # 剪枝(可选) if 不满足条件: continue 做选择(路径.add(选择)) backtrack(路径, 选择列表) 撤销选择(路径.pop())

全排列(LeetCode 46)

def permute(nums): result = [] used = [False] * len(nums) def backtrack(path): if len(path) == len(nums): result.append(path[:]) return for i in range(len(nums)): if used[i]: continue used[i] = True path.append(nums[i]) backtrack(path) path.pop() used[i] = False backtrack([]) return result
一句话总结:回溯 = 递归 + 循环 + 撤销;排列用 used 数组,组合/子集用 start 索引。

中级深入

子集(LeetCode 78)

def subsets(nums): result = [] def backtrack(start, path): result.append(path[:]) # 每个节点都收集 for i in range(start, len(nums)): path.append(nums[i]) backtrack(i + 1, path) # i+1 保证不重复 path.pop() backtrack(0, []) return result

组合总和(LeetCode 39)

# 无重复元素,可重复使用 def combination_sum(candidates, target): result = [] def backtrack(start, path, remain): if remain == 0: result.append(path[:]) return if remain < 0: return for i in range(start, len(candidates)): path.append(candidates[i]) backtrack(i, path, remain - candidates[i]) # i 不是 i+1(可重复) path.pop() backtrack(0, [], target) return result

全排列 II(含重复元素,LeetCode 47)

def permute_unique(nums): nums.sort() # 排序让相同元素相邻 result = [] used = [False] * len(nums) def backtrack(path): if len(path) == len(nums): result.append(path[:]) return for i in range(len(nums)): if used[i]: continue # 去重:相同元素,前一个没用过 → 跳过 if i > 0 and nums[i] == nums[i-1] and not used[i-1]: continue used[i] = True path.append(nums[i]) backtrack(path) path.pop() used[i] = False backtrack([]) return result
中级要点:子集/组合用 start 索引避免重复;含重复元素先排序,用 used 或 start 去重。

高级拓展

N 皇后(LeetCode 51)

def solve_n_queens(n): result = [] board = [['.'] * n for _ in range(n)] cols = set() # 列 diag1 = set() # 主对角线(row-col) diag2 = set() # 副对角线(row+col) def backtrack(row): if row == n: result.append([''.join(r) for r in board]) return for col in range(n): if col in cols or (row-col) in diag1 or (row+col) in diag2: continue board[row][col] = 'Q' cols.add(col) diag1.add(row - col) diag2.add(row + col) backtrack(row + 1) board[row][col] = '.' cols.remove(col) diag1.remove(row - col) diag2.remove(row + col) backtrack(0) return result

回溯 vs 动态规划

维度回溯动态规划
问题类型求所有解(排列/组合/子集)求最优解(最大/最小/计数)
核心操作选择 → 递归 → 撤销状态转移方程
时间复杂度通常指数级通常多项式级
优化手段剪枝记忆化搜索、状态压缩

实战场景

场景:电话号码的字母组合(LeetCode 17)

def letter_combinations(digits): if not digits: return [] phone = { '2': 'abc', '3': 'def', '4': 'ghi', '5': 'jkl', '6': 'mno', '7': 'pqrs', '8': 'tuv', '9': 'wxyz' } result = [] def backtrack(index, path): if index == len(digits): result.append(''.join(path)) return for ch in phone[digits[index]]: path.append(ch) backtrack(index + 1, path) path.pop() backtrack(0, []) return result

面试模拟

面试官:回溯算法的核心框架是什么?

你:回溯三部曲:1)做选择(加入路径);2)递归进入下一层;3)撤销选择(回溯)。本质是多叉树的 DFS。排列问题用 used 数组标记已选元素,组合/子集用 start 索引保证不重复选取。含重复元素时先排序,再通过 used 或 start 去重。

面试官:全排列有重复元素怎么去重?

你:先排序让相同元素相邻。在回溯的 for 循环中,如果 nums[i] == nums[i-1] 且 !used[i-1](前一个相同元素没被使用),说明是同一层的重复选择,跳过。注意是 !used[i-1] 而不是 used[i-1],前者是树层去重,后者是树枝去重。