中等
给你一个 无重复元素 的整数数组 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 <= 302 <= candidates[i] <= 40candidates的所有元素 互不相同1 <= target <= 40
📝 核心笔记:组合总和 (Combination Sum)
1. 核心思想 (一句话总结)
"无限续杯:选了还能再选,不选就永远拜拜。" 因为题目允许数字重复使用 ,所以在"选当前数"的分支里,递归下标依然是 i (原地踏步),而不是 i + 1。只有决定"不选"了,才移动到下一个数。
💡 图像记忆 (自助餐):
- 你站在一道菜(
candidates[i])面前。 - 抉择 A (选) :拿一份放盘子里,不走,继续盯着这道菜看(因为还能再拿一份)。
- 抉择 B (不选) :摇摇头,走向下一道菜(
i + 1),并且再也不回头看这道菜了。
2. 算法流程 (三步走)
- 触底 (Base Case):
-
left == 0:钱正好花光 -> 记录答案。left < 0或i 越界:钱超支了 或 没菜了 -> 回退。
- 分支一:不选 (Skip):
-
- 直接跳到下个索引:
dfs(i + 1, left)。
- 直接跳到下个索引:
- 分支二:选 (Pick):
-
- 扣钱:
left - val。 - 原地递归 :
dfs(i, new_left)(关键点!)。 - 回溯:把刚才拿的拿出来。
- 扣钱:
🔍 代码回忆清单 (带注释版)
// 题目:LC 39. Combination Sum
class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> ans = new ArrayList<>();
List<Integer> path = new ArrayList<>();
dfs(0, target, candidates, ans, path);
return ans;
}
// i: 当前盯着第几个数看
// left: 还需要凑多少钱
private void dfs(int i, int left, int[] candidates, List<List<Integer>> ans, List<Integer> path) {
// 1. Base Case: 成功凑出
if (left == 0) {
ans.add(new ArrayList<>(path)); // 📸 拍照留念
return;
}
// 2. Base Case: 失败 (越界 或 减过头了)
if (i == candidates.length || left < 0) {
return;
}
// 3. 决策 A: 不选当前数 (Skip)
// 既然不选它,那就以后永远不选它了,去下一个
dfs(i + 1, left, candidates, ans, path);
// 4. 决策 B: 选当前数 (Pick)
path.add(candidates[i]);
// ⭐️ 关键点:递归 i 而不是 i+1,表示"还可以再选它"
dfs(i, left - candidates[i], candidates, ans, path);
path.remove(path.size() - 1); // 回溯
}
}
⚡ 快速复习 CheckList (易错点)
-
\] **为什么是** **dfs(i)****不是** **dfs(i+1)****?**
-
- 这是本题核心。题目说"可以无限制重复被选取"。
dfs(i)= 选了 2,下次还能选 2。dfs(i+1)= 选了 2,下次只能选 3 (这是"子集"问题的逻辑)。
-
\] **剪枝优化 (Pruning)?**
-
- 如果面试官问怎么优化,可以说:先对数组排序。
- 在"选"的分支里,如果
left - candidates[i] < 0,因为数组有序,后面的数更大,肯定更减不动,直接return或break,不用再递归了。
-
\] **对比 For 循环写法?**
-
- 选/不选 (你的代码):二叉树,深度可能很深(一直选同一个数)。
- For 循环枚举 :N 叉树,每次从
i开始循环。 - 两种都可以,选/不选的逻辑在背包问题中更通用。
🖼️ 数字演练
输入 candidates = [2, 3], target = 5
- DFS(0, 5): 面对 2。
- Skip 2 :
dfs(1, 5)-> 面对 3。
-
- Skip 3 :
dfs(2, 5)-> 没数了,Return。 - Pick 3 :
path=[3],left=2,dfs(1, 2)(还面对 3)。
- Skip 3 :
-
-
- Pick 3 :
left=-1-> Return。
- Pick 3 :
-
- Pick 2 :
path=[2],left=3,dfs(0, 3)(还面对 2)。
-
- Skip 2 :
dfs(1, 3)-> 面对 3。
- Skip 2 :
-
-
- Pick 3 :
path=[2, 3],left=0-> Bingo! Add [2, 3]。
- Pick 3 :
-
-
- Pick 2 :
path=[2, 2],left=1,dfs(0, 1)(还面对 2)。
- Pick 2 :
-
-
- ... (再选 2 就超了)
-
(最终结果:找到了 [2, 3])