GitHub_Trending/leetcode1/leetcode组合总和II:回溯法的排序与去重
【免费下载链接】leetcode Leetcode solutions 项目地址: https://gitcode.com/GitHub_Trending/leetcode1/leetcode
问题引入与核心挑战
在LeetCode算法题库中,组合总和II(Combination Sum II)是一道经典的回溯算法应用题。该问题要求从包含重复元素的候选数组中找出所有不重复的组合,使得组合中元素的和等于目标值,且每个元素只能使用一次。其核心难点在于如何在回溯过程中有效去重,避免生成重复的组合结果。
问题具体描述
- 输入:候选数组
candidates(可能包含重复元素)和目标值target - 输出:所有不重复的组合列表,其中每个组合元素之和等于
target - 约束:每个元素在组合中只能使用一次
重复组合产生的根源
当候选数组存在重复元素时,即使严格遵循"每个元素仅使用一次"的规则,仍可能通过不同的选择路径生成相同的组合。例如对于 candidates = [1,1,2], target = 3:
- 选择第一个1和2 → [1,2]
- 选择第二个1和2 → [1,2]
这两种不同的选择路径产生了完全相同的组合结果,需要通过特定的去重机制予以排除。
解决方案:排序+回溯去重
算法整体框架
解决组合总和II问题的高效方法是采用排序预处理+回溯搜索+同层去重的策略,具体步骤如下:
关键技术点解析
-
排序预处理
- 对候选数组进行升序排序,使重复元素相邻,为后续去重提供条件
- 排序后可利用元素大小关系进行剪枝优化
-
同层去重机制
- 核心判断条件:
i > start && candidates[i] == candidates[i-1] - 当
i > start时,说明当前元素与同层前一个元素相同,需要跳过以避免重复组合
- 核心判断条件:
-
剪枝优化
- 当当前元素大于剩余目标值时,可直接终止循环(因数组已排序,后续元素更大)
核心代码实现(C语言)
void backtrack(int *candidates, int candidatesSize, int target, int start, int *current, int currentSize, int **result, int *resultSize, int *returnColumnSizes) {
if (target == 0) {
// 找到符合条件的组合,保存结果
result[*resultSize] = (int *)malloc(currentSize * sizeof(int));
returnColumnSizes[*resultSize] = currentSize;
for (int i = 0; i < currentSize; i++) {
result[*resultSize][i] = current[i];
}
(*resultSize)++;
return;
}
for (int i = start; i < candidatesSize; i++) {
// 去重核心判断:跳过同层重复元素
if (i > start && candidates[i] == candidates[i - 1]) {
continue;
}
// 剪枝优化:当前元素大于剩余目标值,直接终止循环
if (candidates[i] > target) {
break;
}
current[currentSize] = candidates[i];
// 递归调用:注意参数i+1确保每个元素只使用一次
backtrack(candidates, candidatesSize, target - candidates[i], i + 1, current, currentSize + 1, result, resultSize, returnColumnSizes);
}
}
int compare(const void *a, const void *b) {
return (*(int *)a - *(int *)b);
}
int** combinationSum2(int* candidates, int candidatesSize, int target, int* returnSize, int** returnColumnSizes) {
// 排序预处理
qsort(candidates, candidatesSize, sizeof(int), compare);
int **result = (int **)malloc(1000 * sizeof(int *));
int *current = (int *)malloc(candidatesSize * sizeof(int));
*returnColumnSizes = (int *)malloc(1000 * sizeof(int));
int resultSize = 0;
backtrack(candidates, candidatesSize, target, 0, current, 0, result, &resultSize, *returnColumnSizes);
free(current);
*returnSize = resultSize;
return result;
}
多语言实现对比
Ruby实现
def combination_sum2(candidates, target)
candidates.sort!
result = []
backtrack(candidates, target, 0, [], result)
result
end
def backtrack(candidates, target, start, path, result)
if target == 0
result << path.dup
return
end
(start...candidates.size).each do |i|
# 去重逻辑
next if i > start && candidates[i] == candidates[i-1]
break if candidates[i] > target
path << candidates[i]
backtrack(candidates, target - candidates[i], i + 1, path, result)
path.pop
end
end
Go实现
import "sort"
func combinationSum2(candidates []int, target int) [][]int {
sort.Ints(candidates)
var result [][]int
var path []int
backtrack(candidates, target, 0, path, &result)
return result
}
func backtrack(candidates []int, target, start int, path []int, result *[][]int) {
if target == 0 {
temp := make([]int, len(path))
copy(temp, path)
*result = append(*result, temp)
return
}
for i := start; i < len(candidates); i++ {
if i > start && candidates[i] == candidates[i-1] {
continue
}
if candidates[i] > target {
break
}
path = append(path, candidates[i])
backtrack(candidates, target - candidates[i], i + 1, path, result)
path = path[:len(path)-1]
}
}
去重逻辑深度解析
为什么i > start是必要条件?
i > start 这个条件确保了只跳过同层的重复元素,而允许不同层使用相同元素。通过以下遍历过程可以清晰理解:
如果缺少 i > start 条件,将会错误地过滤掉不同层的必要元素,导致遗漏有效组合。
去重前后对比
未去重时可能产生的重复组合:
[1,2], [1,2], [1,1,1], [1,1,1]
经过去重处理后的结果:
[1,2], [1,1,1]
性能优化分析
不同策略性能对比
| 优化策略 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 未排序+无剪枝 | O(2ⁿ) | O(n) | 小规模数据 |
| 排序+剪枝 | O(n×2ⁿ) | O(n) | 所有情况 |
| 排序+剪枝+去重 | O(n×2ⁿ) | O(n) | 含重复元素 |
剪枝效果可视化
对于 candidates = [1,2,3,4,5,6,7], target = 8:
- 未剪枝:需要遍历所有可能的组合
- 剪枝后:当遇到元素大于剩余目标值时立即停止,减少大量无效递归
常见错误案例分析
错误1:去重判断缺少i > start
// 错误代码
if (candidates[i] == candidates[i - 1]) { // 缺少i > start
continue;
}
后果:会过滤掉所有重复元素,包括不同层的必要元素,导致遗漏有效组合。
错误2:递归时传递i而非i+1
// 错误代码
backtrack(..., i, ...); // 应该是i+1
后果:元素可被重复使用,违反"每个元素只能使用一次"的题目限制。
错误3:在递归前未排序
// 错误代码
// qsort(candidates, candidatesSize, sizeof(int), compare); // 遗漏排序
后果:重复元素可能不相邻,导致去重逻辑失效,生成重复组合。
总结与拓展应用
组合总和II问题的核心在于排序预处理和同层去重,掌握这一方法后,可以解决一系列类似的回溯去重问题:
- 子集II(LeetCode 90):找出所有不重复的子集
- 全排列II(LeetCode 47):找出所有不重复的全排列
- 电话号码的字母组合(LeetCode 17):处理重复数字映射
回溯算法的本质是深度优先搜索(DFS)与剪枝的结合,通过合理的去重和剪枝策略,可以大幅提高算法效率。在实际应用中,需特别注意去重逻辑的正确性,避免多去或漏去。
掌握本文介绍的排序+同层去重方法,你将能够轻松应对各类回溯去重问题,显著提升算法解题能力。收藏本文,下次遇到类似问题可直接对照参考!
【免费下载链接】leetcode Leetcode solutions 项目地址: https://gitcode.com/GitHub_Trending/leetcode1/leetcode
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



