给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用一次。
说明:
所有数字(包括目标数)都是正整数。
解集不能包含重复的组合。
示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
所求解集为:
[
[1, 7],
[1, 2, 5],
[2, 6],
[1, 1, 6]
]
示例 2:
输入: candidates = [2,5,2,1,2], target = 5,
所求解集为:
[
[1,2,2],
[5]
]
分析:本题是经典的回溯+递归的做法,即对于每个数都有选择和不选择两个选择,但是如果只是这样选择,答案会出现重复 比如[2,2],target = 2,【2】的答案会出现两次。即第一次选取第一个2,第二次选取第二个2.
为了解决重复的问题,我们可以开创一个列表,里面存储的是一位数组,数组下标为0存储元素的值,下标为1存储该数出现的次数。
假设不同的数共有m个,因此我们可以遍历,假设一个数的出现次数是i,那么我们就可以考虑对该数选择0次,1次…i次的情况,分别进行递归,这样就不会出现重复的情况,当当前位置选择到了m个时,说明已经递归结束,没有多余的数在可以选,当rest <0时也说明此时无法凑到rest,也可以进行退出。
这样一来,我们就可以不重复地枚举所有的组合了。
我们还可以进行什么优化(剪枝)呢?一种比较常用的优化方法是,我们将 freq 根据数从小到大排序,这样我们在递归时会先选择小的数,再选择大的数。这样做的好处是,当我们递归到dfs(pos,rest) 时,如果 freq[pos][0] 已经大于 rest,那么后面还没有递归到的数也都大于 rest,这就说明不可能再选择若干个和为rest 的数放入列表了。此时,我们就可以直接回溯。
class Solution {
List<List<Integer>> ans = new ArrayList<List<Integer>>();
//用来记录每个待选元素的值和出现的次数
List<int[] > freq = new ArrayList<int[]>();
//当前的序列
List<Integer> seq = new ArrayList<Integer>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
//先对candidates进行排序
Arrays.sort(candidates);
for(int num : candidates){
int size = freq.size();
if(freq.isEmpty() || freq.get(size - 1)[0]!= num){
//这里要记住这种写法
freq.add(new int[]{num,1});
}else{
freq.get(size - 1)[1]++;
}
}
//开始回溯
dfs(0,target);
return ans;
}
public void dfs(int pos,int rest){
if(rest == 0){
ans.add(new ArrayList<Integer>(seq));
}else if(rest < 0 || pos == freq.size()){ //这里需要思考 当pos == freq.size()时,为什么要退出,因为总共有这么多数
return ;
}else{
//开始遍历 对于每个数若出现次数为n,则可以选0次,1次...选n次的做法
//对于选0次的时候
dfs(pos + 1,rest);
//这是对于每个数最多选择的次数,第一个表示的是 如果超过这个次数 其和就会大于rest 没必要,所以剪枝
int most = Math.min(rest / freq.get(pos)[0],freq.get(pos)[1]);
for(int i = 1;i <= most;i++){
seq.add(freq.get(pos)[0]);
//这里是选择 i个第pos个数
dfs(pos + 1,rest - i*freq.get(pos)[0]);
//执行完上一个dfs 推出的时候
//一定会执行到这里 继续下一个i的选择
}
//这里要回溯
//将之前添加到seq中的most个数值删除掉,到这里肯定说明前面most个都已经添加到seq中
for(int i = 1;i <= most;i++){
seq.remove(seq.size() - 1);
}
}
}
}