1. 题目解析与核心思路
Leetcode 138题实际上并不存在,根据输入内容判断应为两个经典回溯问题的组合:"全排列"和"组合总和"。这两类问题都是回溯算法的典型应用场景,也是面试中的高频考点。
全排列问题(如Leetcode 46题)要求给定一个不含重复数字的数组,返回所有可能的排列。例如输入[1,2,3],输出应为[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]。
组合总和问题(如Leetcode 39题)则是给定一个无重复元素的数组和一个目标数,找出所有可以使数字和为目标数的组合。例如candidates = [2,3,6,7], target = 7,解集为[[7],[2,2,3]]。
1.1 回溯算法框架
这两个问题的共同解法都是回溯算法,其核心框架如下:
python复制def backtrack(路径, 选择列表):
if 满足结束条件:
结果.append(路径)
return
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择
这个框架就像走迷宫:每次走到岔路口都尝试一条路(做选择),走到死胡同就返回(撤销选择),找到出口就记录下来(满足结束条件)。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 全排列问题深度解析
2.1 基础解法实现
以Leetcode 46题为例,标准解法如下:
python复制def permute(nums):
res = []
def backtrack(path, choices):
if not choices:
res.append(path[:])
return
for i in range(len(choices)):
path.append(choices[i])
backtrack(path, choices[:i]+choices[i+1:])
path.pop()
backtrack([], nums)
return res
这个解法的时间复杂度是O(n*n!),因为共有n!种排列,每次生成排列需要O(n)时间。空间复杂度主要是递归栈的O(n)。
2.2 关键优化技巧
- 交换法优化空间:通过原地交换元素避免切片操作
python复制def permute(nums):
res = []
def backtrack(first):
if first == len(nums):
res.append(nums[:])
return
for i in range(first, len(nums)):
nums[first], nums[i] = nums[i], nums[first]
backtrack(first + 1)
nums[first], nums[i] = nums[i], nums[first]
backtrack(0)
return res
- 处理含重复元素的情况(Leetcode 47题):
需要在回溯前排序,并添加剪枝条件:
python复制if i > 0 and nums[i] == nums[i-1] and not used[i-1]:
continue
3. 组合总和问题详解
3.1 无重复元素的组合总和
以Leetcode 39题为例,标准解法:
python复制def combinationSum(candidates, target):
res = []
candidates.sort()
def backtrack(start, path, remain):
if remain == 0:
res.append(path[:])
return
for i in range(start, len(candidates)):
if candidates[i] > remain:
break
path.append(candidates[i])
backtrack(i, path, remain - candidates[i])
path.pop()
backtrack(0, [], target)
return res
这个解法通过排序和提前break实现了剪枝优化,时间复杂度最坏是O(n^(target/min)),空间复杂度O(target/min)。
3.2 进阶变形问题
-
每个数字只能用一次(Leetcode 40题):
只需将backtrack(i,...)改为backtrack(i+1,...) -
组合总数而非具体组合(Leetcode 377题):
可以转换为动态规划问题,dp[i]表示和为i的组合数:
python复制dp = [0] * (target + 1)
dp[0] = 1
for i in range(1, target+1):
for num in nums:
if i >= num:
dp[i] += dp[i - num]
4. 回溯算法通用优化策略
4.1 剪枝技巧总结
- 排序预处理:对于组合类问题,先排序可以方便后续剪枝
- 和值提前判断:在组合总和中,如果当前和已经超过target可以直接终止
- 重复元素跳过:对于排列问题,遇到重复元素需要特殊处理
- 索引控制:通过控制遍历起始索引避免重复组合
4.2 常见错误排查
- 结果中出现空列表:通常是因为结束条件判断错误
- 组合重复:检查是否正确处理了起始索引
- 栈溢出:确保递归有终止条件,且问题规模合理
- 结果遗漏:检查剪枝条件是否过于严格
5. 实战应用与扩展
5.1 面试常见变种题
- 子集问题(Leetcode 78)
- 括号生成(Leetcode 22)
- 单词搜索(Leetcode 79)
- N皇后问题(Leetcode 51)
5.2 工程实践中的应用
- 电商平台的商品组合推荐
- 游戏中的装备合成系统
- 路径规划中的多方案生成
- 测试用例的自动化生成
关键提示:在面试中遇到回溯问题时,建议先明确三点:
- 路径是什么(如何表示当前选择)
- 选择列表是什么(每一步有哪些选项)
- 结束条件是什么(何时将路径加入结果)
回溯算法的核心在于理解"尝试-回退"的过程,就像走迷宫时用粉笔做标记。在实际编码时,我习惯先用简单的测试用例(如3个元素)手动模拟执行过程,确保逻辑正确后再处理边界条件。对于排列问题,交换法通常比选择法更高效;而对于组合问题,排序后的剪枝能显著提升性能。
