回溯算法解决组合总和II问题与优化策略
1. 问题背景与理解
"组合总和II"是LeetCode上经典的算法题目(编号40),属于回溯算法的典型应用场景。这道题与基础版的"组合总和"(39题)相比,最大的区别在于候选数组中可能包含重复元素,但要求最终解集中不能包含重复的组合。这在实际开发中对应着很多真实场景,比如电商平台的优惠券组合推荐、投资组合优化等需要避免重复方案的业务需求。
我第一次遇到这个问题时,直观想到的是直接用标准回溯模板,结果发现会生成大量重复解。比如候选数组[1,1,2,5],目标和为8时,[1,2,5]会重复出现两次。这让我意识到需要设计更精细的剪枝策略。
2. 算法核心思路解析
2.1 回溯算法框架
回溯算法的基本框架包含三个关键部分:
- 路径记录:保存当前已选择的元素
- 选择列表:当前可选的元素范围
- 结束条件:达到目标或无法继续选择
对于组合总和问题,标准模板如下:
def backtrack(path, choices, target): if target == 0: result.append(path) return for i in range(len(choices)): if choices[i] > target: continue backtrack(path+[choices[i]], choices[i:], target-choices[i])2.2 去重关键策略
当数组包含重复元素时,上述方法会产生重复解。我们需要两个关键改进:
- 排序预处理:先对数组排序,使相同元素相邻
- 层级去重:在同一层级遍历时,跳过与前一个元素相同的候选
具体实现时要注意:
去重判断应该是
i > start_index and candidates[i] == candidates[i-1],而不是简单的相邻比较。这样才能保证不同层级可以选取相同值元素。
3. 完整实现与优化
3.1 Python实现详解
def combinationSum2(candidates, target): candidates.sort() res = [] def backtrack(start, path, remaining): if remaining == 0: res.append(path.copy()) return for i in range(start, len(candidates)): # 剪枝:剩余值不足 if candidates[i] > remaining: break # 去重关键:跳过同一层级的重复元素 if i > start and candidates[i] == candidates[i-1]: continue path.append(candidates[i]) backtrack(i+1, path, remaining - candidates[i]) path.pop() backtrack(0, [], target) return res时间复杂度分析:
- 最坏情况O(2^n):每个元素都有选或不选两种可能
- 实际通过剪枝会好很多
空间复杂度:
- O(n):递归栈深度不超过数组长度
3.2 关键优化点
- 提前排序:不仅为去重,也为后续剪枝创造条件
- 剩余值剪枝:当当前候选大于剩余目标值时,可提前终止循环
- 路径拷贝优化:只在加入结果时复制path,减少内存操作
4. 应用场景与变种
4.1 实际工程应用
- 电商促销组合:从可用优惠券中找出总和等于订单金额的组合,避免重复方案
- 资源分配:将有限资源分配给多个项目,每个项目有最小投入要求
- 菜单规划:从食材中选择搭配,正好用完库存且营养达标
4.2 常见变种题型
- 限制组合长度:如要求解的个数必须是k个元素
- 多条件组合:除了数值和,还需满足其他约束条件
- 概率最大化:每个元素有概率值,求概率乘积最大的组合
5. 调试与边界情况
5.1 常见错误排查
重复解问题:
- 检查是否漏了排序步骤
- 确认去重条件是
i > start而非i > 0
遗漏有效解:
- 检查递归时是否错误地跳过了可用的候选
- 确认剪枝条件是否正确(
>还是>=)
无限递归:
- 确保每次递归的start参数正确递增
- 检查剩余值更新是否正确
5.2 测试用例设计
有效测试应包含:
tests = [ # 基础案例 ([2,3,5], 8, [[3,5]]), # 含重复元素 ([1,1,2,5], 8, [[1,2,5],[1,1,2,4]]), # 无解情况 ([2,4,6], 7, []), # 空输入 ([], 5, []), # 目标为0 ([1,2], 0, [[]]) ]6. 算法扩展思考
对于特别大的候选集(如n>100),标准回溯可能不够高效。可以考虑以下优化方向:
动态规划预处理:
- 先用DP找出可能的和值组合
- 再反向追踪具体元素组合
并行计算:
- 将候选集分割为多个子集
- 在不同线程/进程中分别处理
记忆化搜索:
- 缓存中间结果
- 避免重复计算相同子问题
在实际面试中,建议先给出标准回溯解法,再讨论优化可能。面试官通常更关注对算法本质的理解而非极端优化。