1. 关于回溯
tbc
回溯的应用场景
- 组合
- 分割
- 子集
- 要求:给你一个整数数组 nums ,返回该数组所有可能的子集(幂集)。解集不能包含重复的子集
- 思路:path不再是各节点组成的路线,而是每个节点储存的组合
- 全排列
- 要求:给定一个数组 nums ,返回其所有可能的全排列
- 思路:
- 差异:
- 需要return的path是每个节点还是多个节点组成的路径?
- 递归剩余的nums,是nums[i]序列之后的部分,还是除了nums[i]的所有其余部分?
- 怎么去重?
- 待施工
- 待强化:
- 需要巩固的:arraylist, linkedlist到底有什么不同?在数据结构上,在方法调用上。|| 哈希表和hashset的不同?
2. 例题
lc78 子集I
思路
参数:老三样
终止条件:游标走到nums尽头再开始return
单层递归逻辑:每次递归都要储存该节点的path记录到result
待解决问题
如果把result.add(new ArrayList<>(path))放在for循环里实现呢?除了少了空集还有什么不同?
代码实现
class Solution {
public LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> result = new ArrayList<>();
public List<List<Integer>> subsets(int[] nums) {
backtrack(nums,0);
return result;
}
public void backtrack(int[] nums, int start){
result.add(new ArrayList<>(path));//每次递归都要储存当前节点path的值
if(start >= nums.length){
return;
}
//注意:每个节点都是一个组合,而不是走到叶节点尽头、才生成path个组合
for(int i=start;i<nums.length;i++){
path.add(nums[i]);
backtrack(nums,i+1);
path.removeLast();
}
}
}
lc78 子集II
思路
其他同上。
78子集和40组合总和的变体
78:解决子集的return问题(每次递归都要return,而不是终止时return)
40:解决去重问题(排序成为有序数组,把当前值和前一位比较,若重复则跳过,不重复再进入循环)
代码实现
class Solution {
public LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> result = new ArrayList<>();
public List<List<Integer>> subsetsWithDup(int[] nums) {
Arrays.sort(nums);
backtrack(nums,0);
return result;
}
public void backtrack(int[] nums, int start ){
result.add(new ArrayList<>(path));
if(start > nums.length){
return;
}
for(int i=start;i<nums.length;i++){
if(i> start && nums[i]==nums[i-1]){
continue;
}else{
path.add(nums[i]);
backtrack(nums,i+1);
path.removeLast();
}
}
}
}
lc46 全排列I
思路
其他同上。
引入一个开关used:标记不同的index(及其对应的元素)是否用过,用过则打开开关true
易错点
- 回退与其他动作一荣俱荣一损俱损:如sum+,则回退后需要sum-;如标记used状态,则回退后需要打回unused
代码实现
class Solution {
public LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> result = new ArrayList<>();
public List<List<Integer>> permute(int[] nums) {
boolean[] used = new boolean[nums.length]; //注意这个used只能放在这里,因为nums是一个输入变量
backtrack(nums,0,used);
return result;
}
public void backtrack(int[] nums, int start,boolean[] used){
if(start==nums.length){
result.add(new ArrayList<>(path));
return;
}
for(int i=0;i<nums.length;i++){
if(! used[i]){
path.add(nums[i]);
used[i]=true;
backtrack(nums,i+1,used);
path.removeLast();
used[i]=false; //注意!!如果回退,那标记的used也要还原为unused!
}
}
}
}
lc47 全排列II
思路
与上文相比,增加了去重的需求。46和40的缝合。
46:引入used开关标记已经用过的元素
40:数组中元素重复问题:arrays.sort(nums),然后nums[i]==nums[i-1]看是否重复,重复则跳过
在递归时,跳过/不录入两种情况:
1. 已经used
2. 2. 元素和之前重复,若反复使用会使path重合
代码实现
class Solution {
public LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> result = new ArrayList<>();
public List<List<Integer>> permuteUnique(int[] nums) {
boolean[] used=new boolean[nums.length];
Arrays.sort(nums);
backtrack(nums,used);
return result;
}
public void backtrack(int[]nums, boolean[] used){
if(path.size()==nums.length){
result.add(new ArrayList<>(path));
return;
}
for(int i=0;i<nums.length;i++){
//情况1:如果前一个元素没用过但是两个值相同则跳过
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1]==false) {
continue;
}
//情况2:如果现在元素用过则跳过。可以合并写,更简单。
if(used[i]){
continue;
}else{
path.add(nums[i]);
used[i]=true;
backtrack(nums,used);
path.removeLast();
used[i]=false;
}
}
}
}