内容介绍
给定一个候选人编号的集合
candidates
和一个目标数target
,找出candidates
中所有可以使数字和为target
的组合。
candidates
中的每个数字在每个组合中只能使用 一次 。注意:解集不能包含重复的组合。
示例 1:
输入: candidates =[10,1,2,7,6,1,5]
, target =8
, 输出: [ [1,1,6], [1,2,5], [1,7], [2,6] ]示例 2:
输入: candidates = [2,5,2,1,2], target = 5, 输出: [ [1,2,2], [5] ]提示:
1 <= candidates.length <= 100
1 <= candidates[i] <= 50
1 <= target <= 30
完整代码
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.List;
public class Solution {
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
int len = candidates.length;
List<List<Integer>> res = new ArrayList<>();
if (len == 0) {
return res;
}
// 关键步骤
Arrays.sort(candidates);
Deque<Integer> path = new ArrayDeque<>(len);
dfs(candidates, len, 0, target, path, res);
return res;
}
/**
* @param candidates 候选数组
* @param len 冗余变量
* @param begin 从候选数组的 begin 位置开始搜索
* @param target 表示剩余,这个值一开始等于 target,基于题目中说明的"所有数字(包括目标数)都是正整数"这个条件
* @param path 从根结点到叶子结点的路径
* @param res
*/
private void dfs(int[] candidates, int len, int begin, int target, Deque<Integer> path, List<List<Integer>> res) {
if (target == 0) {
res.add(new ArrayList<>(path));
return;
}
for (int i = begin; i < len; i++) {
// 大剪枝:减去 candidates[i] 小于 0,减去后面的 candidates[i + 1]、candidates[i + 2] 肯定也小于 0,因此用 break
if (target - candidates[i] < 0) {
break;
}
// 小剪枝:同一层相同数值的结点,从第 2 个开始,候选数更少,结果一定发生重复,因此跳过,用 continue
if (i > begin && candidates[i] == candidates[i - 1]) {
continue;
}
path.addLast(candidates[i]);
// 调试语句 ①
// System.out.println("递归之前 => " + path + ",剩余 = " + (target - candidates[i]));
// 因为元素不可以重复使用,这里递归传递下去的是 i + 1 而不是 i
dfs(candidates, len, i + 1, target - candidates[i], path, res);
path.removeLast();
// 调试语句 ②
// System.out.println("递归之后 => " + path + ",剩余 = " + (target - candidates[i]));
}
}
public static void main(String[] args) {
int[] candidates = new int[]{10, 1, 2, 7, 6, 1, 5};
int target = 8;
Solution solution = new Solution();
List<List<Integer>> res = solution.combinationSum2(candidates, target);
System.out.println("输出 => " + res);
}
}
思路详解
代码详解
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
// 初始化结果列表
List<List<Integer>> res = new ArrayList<>();
// 排序候选数组
Arrays.sort(candidates);
// 创建路径栈和当前路径
Deque<Integer> path = new ArrayDeque<>(candidates.length);
// 调用递归函数
dfs(candidates, 0, target, path, res);
// 返回结果列表
return res;
}
private void dfs(int[] candidates, int begin, int target, Deque<Integer> path, List<List<Integer>> res) {
// 如果目标值为0,说明找到了一个有效的组合
if (target == 0) {
res.add(new ArrayList<>(path));
return;
}
// 遍历候选数组
for (int i = begin; i < candidates.length; i++) {
// 大剪枝:如果当前数字减去目标值小于0,则不可能再通过其他数字凑成目标值
if (target - candidates[i] < 0) {
break;
}
// 小剪枝:如果当前数字与上一个数字相同,则不可能通过其他数字凑成目标值
if (i > begin && candidates[i] == candidates[i - 1]) {
continue;
}
// 将当前数字添加到路径中
path.addLast(candidates[i]);
// 递归调用,传递下一层的目标值和下一个起始位置
dfs(candidates, i + 1, target - candidates[i], path, res);
// 回溯,移除当前数字
path.removeLast();
}
}
关键步骤解释
-
初始化:
- 在
combinationSum2
函数中,首先初始化结果列表res
,并对候选数组进行排序。
- 在
-
递归函数定义:
dfs
函数是递归函数,它接受候选数组、当前起始位置、剩余目标值、当前路径栈和结果列表作为参数。
-
递归终止条件:
- 如果剩余目标值等于0,说明找到了一个有效的组合,函数将当前组合保存到结果列表中,并返回。
-
剪枝:
- 在递归函数中,使用大剪枝和小剪枝来优化搜索空间。大剪枝用于剪掉不可能达到目标值的路径,小剪枝用于避免重复的结果。
-
路径更新:
- 在递归函数中,将当前数字添加到路径栈中,并递归调用自身。
-
回溯:
- 在递归函数中,当路径不再有效时,通过移除路径栈中的最后一个元素来实现回溯。
-
结果保存:
- 在递归函数中,当找到一个有效的组合时,将当前组合保存到结果列表中。
-
返回结果:
- 在
combinationSum2
函数中,返回结果列表。
- 在
知识点精炼
-
回溯算法:
- 使用递归方法
dfs
来遍历所有可能的组合。
- 使用递归方法
-
剪枝技术:
- 在递归函数中应用大剪枝和小剪枝技术,以避免不必要的搜索。
-
栈与队列:
- 使用
ArrayDeque
作为路径栈,高效地管理递归过程中的路径。
- 使用
-
排序数组:
- 在
combinationSum2
函数中,对候选数组进行排序,以便在递归过程中应用剪枝。
- 在
-
状态更新:
- 在递归函数中,使用
path
和target
来跟踪当前路径和剩余目标值。
- 在递归函数中,使用
-
数组索引操作:
- 通过数组索引来访问和更新候选数组。
-
递归调用:
- 在递归函数中,通过
begin
和target - candidates[i]
来控制递归的深度和路径。
- 在递归函数中,通过
-
结果保存:
- 在递归函数中,将有效的组合保存到结果列表中。
-
调试语句:
- 在递归函数中,添加调试语句来输出当前路径和剩余目标值,以便于调试。
-
返回值:
- 在
combinationSum2
函数中,返回结果列表。
- 在