全排列问题,子集问题,组合和问题都是经典的回溯问题。
对于子集问题,若无重复元素情况下,可以有下面的通用解法,这个解法的关键在于:第一层递归,得出单个元素集合,第二层递归得到两个元素集合,以此类推。
public List> subsets(int[] nums) {
List> list = new ArrayList<>();
Arrays.sort(nums);
backtrack(list, new ArrayList<>(), nums, 0);
return list;
}
private void backtrack(List> list , List tempList, int [] nums, int start){
list.add(new ArrayList<>(tempList));
for(int i = start; i < nums.length; i++){
tempList.add(nums[i]);
backtrack(list, tempList, nums, i + 1);
tempList.remove(tempList.size() - 1);
}
}
对于带有重复元素的子集问题,也是同样的思路,不同之处在于我们需要过滤掉重复的集合。
首先对数组进行排序,来方便后续过滤重复时候的操作。
接着来分析,按照上边的思路,什么时候会出现重复的集合:对于单个元素集合,我们只需要其出现一次,因此有了skip duplicates的操作;那么,对于两个元素的集合是否也是如此呢?答案是肯定的,因为后续的递归可以看作是没有第一个元素以后的子问题。
public List> subsetsWithDup(int[] nums) {
List> list = new ArrayList<>();
Arrays.sort(nums);
backtrack(list, new ArrayList<>(), nums, 0);
return list;
}
private void backtrack(List> list, List tempList, int [] nums, int start){
list.add(new ArrayList<>(tempList));
for(int i = start; i < nums.length; i++){
if(i > start && nums[i] == nums[i-1]) continue; // skip duplicates
tempList.add(nums[i]);
backtrack(list, tempList, nums, i + 1);
tempList.remove(tempList.size() - 1);
}
}
对于全排列问题,同理,在没有重复元素时,第一层递归枚举所有长度为1的templist,第二层递归再往上加没有用过的元素。但是。。。那个每次检查是否重复使用的操作,貌似很费时间啊。
public List> permute(int[] nums) {
List> list = new ArrayList<>();
// Arrays.sort(nums); // not necessary
backtrack(list, new ArrayList<>(), nums);
return list;
}
private void backtrack(List> list, List tempList, int [] nums){
if(tempList.size() == nums.length){
list.add(new ArrayList<>(tempList));
} else{
for(int i = 0; i < nums.length; i++){
if(tempList.contains(nums[i])) continue; // element already exists, skip
tempList.add(nums[i]);
backtrack(list, tempList, nums);
tempList.remove(tempList.size() - 1);
}
}
}
如果有重复元素时,该怎么办呢?
还是先分析,上边那个做法为什么不可行呢。因为数据有重复,不可能使用contains查重了。如何判断重复数据是否加入templist呢?
对于重复数据连在一起的情况,我们只需要其在最后结果中出现一次,如何保证其只出现一次呢,就是在每次递归中保证templist中的数据次序是唯一的。因此需要一个used数组来指示这个次序,只有在前一个数used情况下,后一个重复数才可能接在其后。
public List> permuteUnique(int[] nums) {
List> list = new ArrayList<>();
Arrays.sort(nums);
backtrack(list, new ArrayList<>(), nums, new boolean[nums.length]);
return list;
}
private void backtrack(List> list, List tempList, int [] nums, boolean [] used){
if(tempList.size() == nums.length){
list.add(new ArrayList<>(tempList));
} else{
for(int i = 0; i < nums.length; i++){
if(used[i] || i > 0 && nums[i] == nums[i-1] && !used[i - 1]) continue;
used[i] = true;
tempList.add(nums[i]);
backtrack(list, tempList, nums, used);
used[i] = false;
tempList.remove(tempList.size() - 1);
}
}
}
还有Combination Sum未完待续。。。
续上。
Combination Sum问题是指从某一数组中找出某几个数使其和为定值Target。
第一种情况是可以重复利用元素,这种情况最为简单,套用模板即可解决。
public List> combinationSum(int[] nums, int target) {
List> list = new ArrayList<>();
Arrays.sort(nums);
backtrack(list, new ArrayList<>(), nums, target, 0);
return list;
}
private void backtrack(List> list, List tempList, int [] nums, int remain, int start){
if(remain < 0) return;
else if(remain == 0) list.add(new ArrayList<>(tempList));
else{
for(int i = start; i < nums.length; i++){
tempList.add(nums[i]);
backtrack(list, tempList, nums, remain - nums[i], i); // not i + 1 because we can reuse same elements
tempList.remove(tempList.size() - 1);
}
}
}
public List> combinationSum2(int[] nums, int target) {
List> list = new ArrayList<>();
Arrays.sort(nums);
backtrack(list, new ArrayList<>(), nums, target, 0);
return list;
}
private void backtrack(List> list, List tempList, int [] nums, int remain, int start){
if(remain < 0) return;
else if(remain == 0) list.add(new ArrayList<>(tempList));
else{
for(int i = start; i < nums.length; i++){
if(i > start && nums[i] == nums[i-1]) continue; // skip duplicates
tempList.add(nums[i]);
backtrack(list, tempList, nums, remain - nums[i], i + 1); //i+1很关键
tempList.remove(tempList.size() - 1);
}
}
}