if(prefix.size()!=0 && candidate.size()==0){
StringBuilder sb = new StringBuilder();
for(int c:prefix){
sb.append(c);
}
if(!set.contains(sb.toString())){
res.add(prefix);
set.add(sb.toString());
}
}
有点傻,我将list转成了String来判断是否重复。。。后来我想了一下,既然要避免重复,那么在list添加元素的时候就应该及时的剪枝,于是我换了一下思路,首先我对数组排序并定义一个数组记录已访问的点,这个数组也作为DFS中的参数之一:
Arrays.sort(nums);
boolean[] used = new boolean[len];
接下来非常重要的一步剪枝:
if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) {
continue;
}
其中,i > 0 是为了保证 nums[i - 1] 有意义,nums[i] == nums[i - 1]表示此次选择的节点和上次选择的一样,即可能出现重复,写 !used[i - 1] 是因为 nums[i - 1] 在深度优先遍历的过程中刚刚被撤销选择,如果没有 !used[i - 1]可能会错误的剪掉在正常回溯(无重复)过程中的有效的枝节。我个人可能更偏向于直接用hashMap去重,因为更简单,但实际上,剪枝来得更快且在实际中很高效。
接下来就是77题组合,唯一的区别是不是全排列,而是只选出部分排列,这个也很简单,和46类似,唯一的区别是DFS递归的出口并不是结果子集中的元素个数刚好是数组总元素的个数,而是等于题目给定的个数K即可。
总结一下,这类排列组合的题目有两个关键点,一个是递归的出口一定要根据题意来选择,一个就是在将满足条件的结果子集加入到结果集时一定要重新创建一个list(深拷贝)!!!虽然我知道但是经常犯错,写在这里警醒一下自己╮(╯▽╰)╭
接下来上例题~
given candidate set [2, 3, 6, 7] and target 7,
A solution set is:
[[7],[2, 2, 3]]
题目挺简单的,也是基本的DFS方法,对于DFS函数,先找到其递归出口:
if(candidates.length == 0) return;
if(target<num) return;
if(target == num){
res.add(new ArrayList<>(list));
return;
}
写得不是很简洁,但是这样就能更好的看出什么情况退出递归函数。
接下来就可以遍历candidates啦:
List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
Arrays.sort(candidates);
dfs(candidates,0,target,new ArrayList<>());
return res;
}
public void dfs(int[] candidates,int num,int target,List<Integer> list){
if(candidates.length == 0) return;
if(target<num) return;
if(target == num){
res.add(new ArrayList<>(list));
return;
}
for(int i=0;i<candidates.length&&target>=(num+candidates[i]);i++){
list.add(candidates[i]);
dfs(candidates,num+candidates[i],target,list);
list.remove(list.size()-1);
}
}
一顿操作猛如虎,一看运行结果就出现了问题。。。我把这个做成了全排列了,比如我把【2,2,3】和【2,3,2】当成两个答案了,所以出错了,因为在每次遍历candidates时我都把之前遍历过的和正在遍历的节点都重复遍历了一遍,而我们仅仅只需要从当前遍历的节点开始向后遍历即可(因为元素可以重复,所以此时当前遍历的节点在下一次遍历中可能还会使用到),我们只需要给DFS函数再加一个参数即可:
public void dfs(int[] candidates,int num,int target,List<Integer> list,int start){
if(candidates.length == 0) return;
if(target<num) return;
if(target == num){
res.add(new ArrayList<>(list));
return;
}
for(int i=start;i<candidates.length&&target>=(num+candidates[i]);i++){
list.add(candidates[i]);
dfs(candidates,num+candidates[i],target,list,i);
list.remove(list.size()-1);
}
}
这样就没问题啦~
再来一个剪枝的练练手(●’◡’●)
For example, given candidate set [10, 1, 2, 7, 6, 1, 5] and target 8,
A solution set is:
[
[1, 7],
[1, 2, 5],
[2, 6],
[1, 1, 6]
]
乍一看和上一题基本上一摸一样!只是每个数字只能用一次了,那这个好说,直接把上一个DFS函数中递归的起点从i改到i+1不就行了:
public void dfs(int[] candidates,int num,int target,List<Integer> list,int start){
if(candidates.length == 0) return;
if(target<num) return;
if(target == num){
res.add(new ArrayList<>(list));
return;
}
for(int i=start;i<candidates.length&&target>=(num+candidates[i]);i++){
list.add(candidates[i]);
dfs(candidates,num+candidates[i],target,list,i+1);
list.remove(list.size()-1);
}
}
看似也没有问题,一运行又出现了重复的子集,这次的重复就是出现了两个完全一样的子集了,很明显是因为数组元素重复导致的,自然可以想到之前讲的排列组合用的剪枝的方法,改法是一样的,在for循环中增加一个判断,DFS函数的参数中加一个visited数组即可:
List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
boolean[] visited = new boolean[candidates.length];
dfs(candidates,0,target,new ArrayList<>(),0,visited);
return res;
}
public void dfs(int[] candidates,int num,int target,List<Integer> list,int start,boolean[] visited){
if(candidates.length == 0) return;
if(target<num) return;
if(target == num){
res.add(new ArrayList<>(list));
return;
}
for(int i=start;i<candidates.length&&target>=(num+candidates[i]);i++){
if (i > 0 && candidates[i] == candidates[i - 1] && !visited[i - 1]) {
continue;
}
visited[i] = true;
list.add(candidates[i]);
dfs(candidates,num+candidates[i],target,list,i+1,visited);
list.remove(list.size()-1);
visited[i] = false;
}
}
是不是很简单,都是固定的套路,理解就行啦o( ̄▽ ̄)ブ