【Leetcode hot 100】39.组合总和
问题链接
39.组合总和
问题描述
给你一个 无重复元素 的整数数组 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
输出: []
提示:
1 <= candidates.length <= 30
2 <= candidates[i] <= 40
candidates
的所有元素 互不相同1 <= target <= 40
问题解答
解题思路
- 核心原理:回溯算法本质是“暴力搜索+回溯撤销”,通过记录“当前路径”和“结果集”,逐步探索所有可能组合,不符合条件时撤销上一步选择(回溯)。
- 去重关键:为避免重复组合(如
[2,2,3]
和[2,3,2]
),递归时从当前索引startIndex
开始遍历,而非从0开始,确保组合内元素按“非递减”顺序选取。 - 剪枝优化:
- 提前对
candidates
排序,当当前元素大于剩余target
时,后续元素更大,直接break
循环(无需继续遍历)。 - 若当前路径和已超过
target
,直接终止递归(避免无效搜索)。
- 提前对
完整代码
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;class Solution {// 存储最终所有合法组合private List<List<Integer>> result = new ArrayList<>();// 存储当前正在构建的组合(路径)private List<Integer> path = new ArrayList<>();public List<List<Integer>> combinationSum(int[] candidates, int target) {// 排序:用于后续剪枝优化(关键步骤)Arrays.sort(candidates);// 调用回溯函数:从索引0开始探索,初始剩余target为传入值backtrack(candidates, target, 0);return result;}/*** 回溯函数* @param candidates 候选数组(已排序)* @param remainingTarget 剩余目标值(当前路径和与target的差值)* @param startIndex 当前遍历的起始索引(避免重复组合)*/private void backtrack(int[] candidates, int remainingTarget, int startIndex) {// 1. 终止条件:找到合法组合(剩余目标值为0)if (remainingTarget == 0) {// 注意:需new新列表,避免path引用被后续修改影响结果result.add(new ArrayList<>(path));return;}// 2. 遍历候选元素(从startIndex开始,避免重复)for (int i = startIndex; i < candidates.length; i++) {int currentNum = candidates[i];// 剪枝:当前元素大于剩余目标值,后续元素更大,直接终止循环if (currentNum > remainingTarget) {break;}// 3. 做选择:将当前元素加入路径,减少剩余目标值path.add(currentNum);// 4. 递归探索:下一层仍从i开始(允许重复选取当前元素)backtrack(candidates, remainingTarget - currentNum, i);// 5. 撤销选择(回溯):移除路径最后一个元素,恢复状态path.remove(path.size() - 1);}}
}
代码解析
- 成员变量
result
:存储所有满足条件的组合(最终返回值)。path
:存储当前正在构建的组合(回溯过程中动态修改)。
- 主方法
combinationSum
- 排序:对
candidates
排序是剪枝的前提,确保后续遍历中元素递增,避免无效搜索。 - 调用回溯:初始从索引
0
开始,剩余目标值为target
。
- 回溯方法
backtrack
- 终止条件:当
remainingTarget == 0
时,说明当前path
的和等于target
,将其加入result
(需new新列表,因为path
是引用类型,直接添加会被后续修改覆盖)。 - 遍历逻辑:从
startIndex
开始遍历,确保组合不重复(若从0开始,会出现[2,3,2]
这类重复组合)。 - 剪枝操作:若
currentNum > remainingTarget
,由于数组已排序,后续元素更大,直接break
循环,减少无效迭代。 - 选择与撤销:
- 选择:
path.add(currentNum)
,同时remainingTarget -= currentNum
(缩小目标范围)。 - 递归:传入
i
而非i+1
,允许重复选取当前元素。 - 撤销:
path.remove(path.size() - 1)
,恢复path
状态,进入下一轮循环探索其他可能。
- 选择:
测试示例验证
示例1:candidates = [2,3,6,7], target = 7
- 排序后:
[2,3,6,7]
。 - 回溯过程:
- 从
2
开始:path=[2]
,剩余5
→ 再选2
:path=[2,2]
,剩余3
→ 再选2
:剩余1
(剪枝)→ 选3
:剩余0
,加入[2,2,3]
。 - 从
3
开始:path=[3]
,剩余4
→ 选3
:剩余1
(剪枝)→ 选6
(剪枝)。 - 从
6
开始:6>7
(剪枝)。 - 从
7
开始:path=[7]
,剩余0
,加入[7]
。
- 从
- 结果:
[[2,2,3],[7]]
(符合预期)。
示例3:candidates = [2], target = 1
- 排序后:
[2]
。 - 遍历:
2>1
(剪枝),无合法组合,返回[]
(符合预期)。
复杂度分析
- 时间复杂度:
O(S)
,S
为所有合法组合的长度之和(每个组合需遍历构建,剪枝后大幅减少无效搜索)。 - 空间复杂度:
O(target)
,递归深度最坏为target/candidates[0]
(如candidates=[2], target=40
,递归深度为20),path
的最大长度也为递归深度。