在上一篇文章中,给出了组合问题的回溯解法,这次说明子集问题的回溯解法,先给出回溯的模板。
private void backtracking(参数1,参数2,...){
if(递归终止条件){
收集结果;
return;
}
for(遍历集合){
处理;
backtracking(参数1,参数2,...); // 递归;
回溯;
}
}
子集
给你一个整数数组 nums,数组中的元素互不相同 。返回该数组所有可能的子集(幂集)。解集不能包含重复的子集。你可以按任意顺序返回解集。
示例 1:
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
class Solution {
List<List<Integer>> ans = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> subsets(int[] nums) {
backtracking(nums, 0);
return ans;
}
private void backtracking(int[] nums, int startIndex){
ans.add(new ArrayList<>(path));
for(int i = startIndex;i < nums.length;i++){
path.add(nums[i]);
backtracking(nums, i + 1);
path.remove(path.size() - 1);
}
}
}
子集II
给你一个整数数组 nums,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。解集不能包含重复的子集。返回的解集中,子集可以按任意顺序排列。
示例 1:
输入:nums = [1,2,2]
输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
int[] used;
public List<List<Integer>> subsetsWithDup(int[] nums) {
Arrays.sort(nums);
used = new int[nums.length];
backtracking(nums, 0);
return res;
}
private void backtracking(int[] nums, int startIndex){
res.add(new ArrayList<>(path));
for(int i = startIndex;i < nums.length;i++){
if(i > 0 && nums[i] == nums[i - 1] && used[i - 1] == 0) continue;
path.add(nums[i]);
used[i] = 1;
backtracking(nums, i + 1);
path.remove(path.size() - 1);
used[i] = 0;
}
}
}
这题和上一题的问题的区别在于集合中的元素有重复,用题一中的解法就会出现重复的子集如示例给出的例子,用上一题解法得到[[],[1],[1,2],[1,2,2],[1,2],[2],[2,2],[2]],出现了两次[1, 2]。值得强调的是,由于子集中的元素无顺序要求,因此,[1, 2]和[2, 1]为同一个集合。总之,这一题需要进行去重操作,也是与上一题的唯一不同点,接下来将详细说明上述代码的执行过程。
首先定义res集合用于收集所有符合条件的子集,path为其中某个子集。值得注意的是,子集问题不需要递归的终止条件,每次backtracking的开始直接对path中的子集进行收集。这一题与上一题的区别在于定义了一个used数组,表示下标i对应的元素是否被使用过,使用过用1表示,反之则为0。使用used数组进行去重操作,要求数组是有序的,因此在subsetsWithDup中对数组进行排序,同时初始化used数组,并调用backtracking,最终返回res集合。
以[4,4,1,4]为例子,首先将数组进行排序得到[1,4,4,4],进入backtracking集时,path为空,此时将空集加入res集合中。之后进入for循环,遍历集合,startIndex从0开始,接着是if判断,改判断是去重的关键逻辑,此时i等于0,不满足if判断,接着将nums[i]加入path,path为[1],used[0] = 1。递归进入下一个backtracking,将集合[1]加入res集合中,进入for循环,此时startIndex从1开始,进入if判断,不满足nums[i] = nums[i - 1],将4加入path集合中,path为[1, 4],used[1] = 1,递归进入下一个backtracking,将集合[1, 4]加入res集合中,接着进入for循环,startIndex从2开始,进入if判断,此时nums[i] = nums[i - 1],但是nums[i - 1] = 1,不等于0,因此,将nums[2]加入path中,path为[1, 4, 4],used[2] = 1,递归进入下一个backtracking中。将[1, 4, 4]加入res集合中,进入for循环,此时startIndex从3开始,进入if判断,nums[i] == nums[i - 1],但是nums[i - 1] = 1,因此将nums[3]加入path中,path为[1, 4, 4, 4],used[3] = 1,递归进入下一个backtracking中,将[1, 4, 4, 4]加入到res集合中,进入for循环,此时startIndex从4开始,不小于nums的长度,不满足for循环条件,递归返回上一个backtracking,将nums[3]从path中移除,path为[1, 4, 4],将used[3]设置为0,i++,i等于4,不小于nums的长度,for循环结束,递归返回上一个backtracking,将nums[2]从path中移除,path为[1, 4],used[2]设置为0,i++,i等于3,进入if判断,nums[i] = nums[i - 1],并且used[2]为0,continue,i++,i等于4,不小于nums的长度,递归返回上一个backtracking,将nums[1]从path中移除,此时path为[1],used[1] = 0。i++,i等于2,nums[i] = nums[i - 1],并且nums[i - 1] = 0,continue,i++,i等于3,也是continue,i++,i等于4,不小于nums长度,for循环结束,递归返回到最初的backtracking,将nums[0]从path中移除,path为空,used[0]设置为0。如此走完了最初backtracking中for循环的第一次,按照上述流程,就能够遍历出所有符合条件的子集。
在上述流程中,if判断阻止了通过for横向添加重复元素,但是允许通过backtracking纵向添加重复元素。