回溯法经典练习:组合总和的深度解析与实战
回溯法经典练习:组合总和的深度解析与实战
引言
在算法世界里,回溯法(Backtracking)是解决 组合、排列、子集 等问题的神器。而 “组合总和”(Combination Sum) 问题,更是回溯算法中的经典代表,几乎是算法面试中的常客。
今天,我就带大家从 直觉理解 到 代码实战,深入解析回溯法如何求解 组合总和,并给出代码详解,带你彻底吃透这个问题!
1. 题目解析
🔹 题目描述
给定一个 无重复元素 的正整数数组 candidates
,以及一个目标值 target
,找到所有可以使数字和为 target
的 唯一组合。
要求:
- 同一个数字可以被无限次使用,但组合的顺序不影响结果(即
{2,2,3}
和{3,2,2}
视为相同组合)。 - 结果集不能包含重复的组合。
示例:
输入: candidates = [2,3,6,7], target = 7
输出: [[2,2,3],[7]]
解释: 2+2+3 = 7, 7 = 7,因此答案是 [[2,2,3],[7]]
2. 直觉思考
这个问题的求解方式,可以类比 爬楼梯:
- 每次选择一个数(例如 2 或 3 或 6 或 7)。
- 判断当前累加和是否等于目标值 target。
- 如果超过 target,则不合法,剪枝回溯。
- 如果刚好等于 target,就加入结果集。
看到 “所有组合”、“无序”、“可以重复选择” 这些关键词,回溯法就是最佳选择!
3. 回溯算法核心框架
回溯法的核心思路是 “递归 + 回溯 + 剪枝”,通常分为以下几个步骤:
- 终止条件:当当前路径的总和等于
target
,将其加入结果集。 - 选择操作:从
candidates
里选择一个数,尝试加入当前路径。 - 递归调用:累加该数后,继续探索下一个可能的组合。
- 回溯撤销选择:探索完该路径后,撤销最后一个选择,回溯到上一步继续尝试其他数。
回溯法的框架:
def backtrack(路径, 选择列表):
if 满足结束条件:
记录结果
return
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表) # 递归
撤销选择 # 回溯
4. 代码实战
🔹 Python 代码
from typing import List
def combinationSum(candidates: List[int], target: int) -> List[List[int]]:
res = [] # 结果集
def backtrack(start, path, total):
# 递归终止条件:找到一个满足条件的组合
if total == target:
res.append(path[:]) # 注意要拷贝 path
return
# 遍历候选数组
for i in range(start, len(candidates)):
num = candidates[i]
# 剪枝:如果 total + num 超过 target,则不再继续
if total + num > target:
continue
# 选择当前数字,递归
path.append(num)
backtrack(i, path, total + num) # 这里 `i` 而不是 `i+1`,因为允许重复选择
path.pop() # 回溯,撤销选择
# 从索引 0 开始搜索
backtrack(0, [], 0)
return res
5. 代码详解
🔸 递归逻辑
backtrack(start, path, total)
start
:控制递归深度,避免重复选择前面的数。path
:记录当前的组合路径。total
:当前路径的累加和。
🔸 关键点
- 循环遍历
candidates
:- 从
start
位置开始,避免重复使用已选过的数。 - 允许重复选择
candidates[i]
,所以backtrack(i, ...)
而不是backtrack(i+1, ...)
。
- 从
- 剪枝优化:
- 当
total + num > target
时,提前终止递归,避免不必要的计算。
- 当
- 回溯撤销选择:
- 递归完一个分支后,撤销
path.append(num)
的选择,继续尝试其他数。
- 递归完一个分支后,撤销
6. 测试代码
candidates = [2, 3, 6, 7]
target = 7
print(combinationSum(candidates, target))
输出结果:
[[2, 2, 3], [7]]
7. 复杂度分析
假设 candidates
长度为 N
,目标值 target
:
- 最坏情况下,回溯的搜索树是一个
N
叉树,高度为target/min(candidates)
。 - 时间复杂度 近似为
O(2^N)
(指数级)。 - 空间复杂度 近似
O(N)
(递归栈空间 + 结果集存储)。
8. 进阶优化
🔹 剪枝优化
在搜索前 先对 candidates
排序,一旦发现 total + num > target
,就可以提前停止当前分支,减少不必要的递归:
candidates.sort() # 先排序,提高剪枝效率
🔹 记忆化搜索
如果 candidates
很多,可以用 字典(哈希表) 存储 target
计算结果,避免重复计算。
9. 结语
回溯法是组合问题的利器,而 “组合总和” 这个问题,是典型的 递归 + 剪枝 题目,掌握它,你就能轻松应对类似的算法题。
🔹 复习总结
- 回溯三步走:选择 -> 递归 -> 回溯。
- 通过
start
参数避免重复,i
位置不变以允许重复选取数字。 - 剪枝优化:如果
total + num > target
,则立即跳过。 - 理解递归树的结构,有助于更好地 Debug。