1. 问题背景与题目解析
今天我们来深入探讨Leetcode第39题"组合总和",这是一道中等难度的回溯算法经典题目。我在刷题过程中发现,这道题看似简单,但实际隐藏着不少值得深究的细节。作为面试中的高频考题,它考察了候选人对回溯算法的理解和应用能力。
题目要求给定一个无重复元素的整数数组candidates和一个目标整数target,找出candidates中所有可以使数字和等于target的唯一组合。candidates中的数字可以无限制重复被选取,但解集不能包含重复的组合。
举个例子:
输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
因为2+2+3=7,7=7,且没有其他组合方式。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 回溯算法基础与解题思路
2.1 回溯算法核心思想
回溯算法是一种通过探索所有可能的候选解来找出所有解的算法。如果候选解被确认不是一个解(或者至少不是最后一个解),回溯算法会通过在上一步进行一些变化来丢弃该解,即"回溯"并尝试其他可能性。
对于组合总和问题,回溯算法的适用性体现在:
- 我们需要枚举所有可能的组合
- 组合中的元素可以重复使用
- 需要避免重复的解(如[2,2,3]和[2,3,2]被视为相同)
2.2 解题步骤分解
解决这个问题的回溯算法可以分为以下几个步骤:
- 对候选数组进行排序(虽然不是必须,但可以优化剪枝)
- 定义一个回溯函数,参数包括当前组合、当前和、起始索引
- 在回溯函数中:
- 如果当前和等于target,将当前组合加入结果集
- 如果当前和超过target,直接返回(剪枝)
- 否则,从起始索引开始遍历候选数组:
- 将当前数字加入组合
- 递归调用回溯函数(注意可以重复使用当前数字)
- 从组合中移除当前数字(回溯)
3. 代码实现与详细注释
3.1 Python实现版本
python复制def combinationSum(candidates, target):
def backtrack(start, path, current_sum):
if current_sum == target:
res.append(path.copy())
return
if current_sum > target:
return
for i in range(start, len(candidates)):
num = candidates[i]
path.append(num)
backtrack(i, path, current_sum + num) # 注意这里传入i而不是i+1,允许重复使用
path.pop() # 回溯
res = []
candidates.sort() # 排序有助于剪枝
backtrack(0, [], 0)
return res
3.2 关键点解析
-
排序的重要性:虽然题目没有要求,但排序后可以更早发现不可能的组合,提前剪枝,提高效率。
-
start参数的作用:这个参数确保了我们不会产生重复的组合(如[2,2,3]和[2,3,2])。通过每次从当前索引开始而不是从头开始,我们保证了组合的顺序性。
-
回溯的三部曲:
- 做出选择(path.append)
- 递归探索(backtrack)
- 撤销选择(path.pop)
-
空间复杂度:主要取决于递归栈的深度,最坏情况下是O(target/min(candidates))。
4. 优化与变种思考
4.1 剪枝优化
在排序的基础上,我们可以进一步优化:
python复制def combinationSum(candidates, target):
def backtrack(start, path, remaining):
if remaining == 0:
res.append(path.copy())
return
for i in range(start, len(candidates)):
num = candidates[i]
if num > remaining: # 提前终止循环
break
path.append(num)
backtrack(i, path, remaining - num)
path.pop()
res = []
candidates.sort()
backtrack(0, [], target)
return res
这个版本使用remaining代替current_sum,减少了每次递归的参数计算量,并且在发现num > remaining时直接break循环,因为后面的数字更大(排序后),不可能满足条件。
4.2 相关变种题目
-
组合总和II(Leetcode 40):candidates中可能有重复元素,但每个数字在每个组合中只能使用一次。
-
组合总和III(Leetcode 216):找出所有相加之和为n的k个数的组合,只能使用数字1到9,且每个数字最多使用一次。
-
组合总和IV(Leetcode 377):考虑顺序不同的序列被视为不同的组合(实际上是排列问题)。
5. 常见错误与调试技巧
5.1 新手常见错误
-
忘记排序:虽然不影响正确性,但会降低效率,无法进行有效剪枝。
-
错误传递start参数:如果递归时传递i+1而不是i,就无法重复使用当前数字,导致漏解。
-
浅拷贝问题:直接res.append(path)而不使用path.copy(),会导致所有结果都指向同一个列表。
-
全局变量污染:在类解法中,如果不正确初始化res,可能导致多次调用时结果累积。
5.2 调试技巧
- 打印递归树:在回溯函数开头打印当前状态,观察递归过程。
python复制print(f"Start: {start}, Path: {path}, Current_sum: {current_sum}")
-
小规模测试:先用小的输入测试,如candidates=[2,3], target=4,手动验证输出。
-
边界条件检查:测试空数组、target为0、candidates包含0等特殊情况。
6. 复杂度分析与实际应用
6.1 时间复杂度分析
回溯算法的时间复杂度通常较难精确计算,我们可以考虑:
-
最坏情况下,每个数字都可能被多次使用,时间复杂度为O(N^(T/M + 1)),其中N是candidates长度,T是target值,M是candidates中的最小值。
-
排序的时间复杂度为O(N log N),通常可以忽略。
6.2 空间复杂度
-
主要取决于递归栈的深度,最坏情况下是O(T/M)。
-
结果存储空间取决于解的个数,可能很大。
6.3 实际应用场景
组合总和问题在实际中有多种应用:
-
商品组合:给定不同面额的优惠券和总金额,找出所有可能的组合方式。
-
资源分配:将固定资源分配给多个项目,每个项目有最小投资额要求。
-
密码破解:尝试不同字符的组合来破解密码(虽然效率不高)。
7. 个人刷题心得
在解决这道题的过程中,我总结了几个关键点:
-
画递归树:对于回溯问题,先在纸上画出递归树,理清思路再编码。
-
剪枝意识:时刻思考如何提前终止不可能的分支,特别是对于大数据量。
-
参数设计:合理设计回溯函数的参数,如remaining比current_sum更直观。
-
测试驱动:先写小的测试用例,确保基本逻辑正确,再逐步扩大。
-
同类题目对比:做完后立即对比组合总和系列的其他题目,找出异同点。
