问题简介
题目描述
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target,找出 candidates 中可以使数字和为目标数 target 的 所有不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个数字可以无限制重复被选取。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。
示例说明
示例 1:
输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。
示例 2:
输入: candidates = [2,3,5], target = 8
输出: [[2,2,2,2],[2,3,3],[3,5]]
示例 3:
输入: candidates = [2], target = 1
输出: []
❌|✅|💡|📌 解题思路
本题是一个典型的 回溯算法(Backtracking) 问题,核心在于:
- 允许重复使用同一元素;
- 要求所有组合不重复(即
[2,2,3]和[2,3,2]视为相同,只保留一种); - 找出所有满足条件的组合。
✅ 思路步骤(回溯法)
- 排序(可选但推荐) :对
candidates排序,便于剪枝(提前终止无效路径)。 - 定义递归函数 :
- 参数:当前路径
path、当前和sum、起始索引start(防止重复组合)。
- 参数:当前路径
- 递归终止条件 :
- 若
sum == target:将当前路径加入结果集; - 若
sum > target:直接返回(剪枝)。
- 若
- 遍历选择 :
- 从
start开始遍历candidates; - 对每个元素
candidates[i]:- 加入路径;
- 递归调用(仍从 i 开始,因为允许重复使用);
- 回溯(移除刚加入的元素)。
- 从
💡 关键点:
start参数确保不会产生重复组合 。例如,若已选2,后续只能从2及之后选,不能回头选3再选2。
🔄 其他解法?
- 动态规划?
理论上可用 DP 求"是否存在组合",但本题要求 输出所有具体组合 ,DP 难以记录路径,因此 回溯是最自然且高效的方法。 - BFS?
可行但空间复杂度高,且去重逻辑复杂,不推荐。
✅ 结论:回溯法是最佳解法。
❌|✅|💡|📌 代码实现
java
class Solution {
List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
Arrays.sort(candidates); // 可选,用于剪枝
backtrack(candidates, target, new ArrayList<>(), 0, 0);
return res;
}
private void backtrack(int[] candidates, int target, List<Integer> path, int sum, int start) {
if (sum == target) {
res.add(new ArrayList<>(path));
return;
}
for (int i = start; i < candidates.length; i++) {
if (sum + candidates[i] > target) break; // 剪枝
path.add(candidates[i]);
backtrack(candidates, target, path, sum + candidates[i], i); // i 而非 i+1
path.remove(path.size() - 1);
}
}
}
go
func combinationSum(candidates []int, target int) [][]int {
sort.Ints(candidates) // 可选,用于剪枝
var res [][]int
var path []int
var backtrack func(sum, start int)
backtrack = func(sum, start int) {
if sum == target {
res = append(res, append([]int(nil), path...))
return
}
for i := start; i < len(candidates); i++ {
if sum + candidates[i] > target {
break // 剪枝
}
path = append(path, candidates[i])
backtrack(sum + candidates[i], i) // i 而非 i+1
path = path[:len(path)-1]
}
}
backtrack(0, 0)
return res
}
💡 注意 Go 中切片复制:
append([]int(nil), path...)创建新切片,避免引用问题。
❌|✅|💡|📌 示例演示
以 candidates = [2,3,6,7], target = 7 为例:
开始:path=[], sum=0, start=0
├─ 选2 → path=[2], sum=2, start=0
│ ├─ 选2 → [2,2], sum=4, start=0
│ │ ├─ 选2 → [2,2,2], sum=6, start=0
│ │ │ └─ 选2 → sum=8 >7 → 剪枝
│ │ └─ 选3 → [2,2,3], sum=7 → ✅ 加入结果
│ └─ 选3 → [2,3], sum=5, start=1
│ └─ 选3 → sum=8 >7 → 剪枝
├─ 选3 → [3], sum=3, start=1
│ └─ ... 最终无解
└─ 选7 → [7], sum=7 → ✅ 加入结果
最终结果:[[2,2,3], [7]]
❌|✅|💡|📌 答案有效性证明
- 完整性 :通过
start参数确保所有可能组合都被探索(从当前位置及之后选),不会遗漏。 - 无重复性 :由于每次递归从
start开始,不会出现[2,3]和[3,2]这类重复。 - 正确性 :只有当
sum == target时才加入结果,且剪枝保证sum <= target。 - 边界处理 :空输入、无解情况(如
target=1, candidates=[2])自然返回空列表。
✅ 满足题目所有要求。
❌|✅|💡|📌 复杂度分析
| 项目 | 分析 |
|---|---|
| 时间复杂度 | O ( N T / M + 1 ) O(N^{T/M + 1}) O(NT/M+1) 其中 N N N 是 candidates 长度, T T T 是 target, M M M 是 candidates 中最小值。 最坏情况下(如 candidates=[1]),递归深度为 target,每层最多 N 个分支。 |
| 空间复杂度 | O ( T / M ) O(T/M) O(T/M) 递归栈深度最大为 target / min(candidates),即路径最大长度。 |
💡 实际中由于剪枝,运行效率远高于理论最坏情况。
❌|✅|💡|📌 问题总结
- 核心思想:回溯 + 剪枝 + 起始索引控制去重。
- 关键技巧 :
- 使用
start避免重复组合; - 排序后提前剪枝(
sum + candidates[i] > target时break); - 注意语言特性(如 Go 切片复制)。
- 使用
- 适用场景:求所有满足条件的组合/排列,允许重复元素,需去重。
✅ 掌握此题,可举一反三解决 组合总和 II、III、IV 等变种问题。
github地址: https://github.com/swf2020/LeetCode-Hot100-Solutions