回溯章节理论基础:
https://programmercarl.com/%E5%9B%9E%E6%BA%AF%E7%AE%97%E6%B3%95%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80.html
491.递增子序列
题目链接:https://leetcode.cn/problems/non-decreasing-subsequences/
思路:
本题求自增子序列,是不能对原数组进行排序的,排完序的数组都是自增子序列了。所以不能使用之前的去重逻辑!
本题给出的示例,还是一个有序数组 [4, 6, 7, 7],这更容易误导大家按照排序的思路去做了。
为了有鲜明的对比,我用[4, 7, 6, 7]这个数组来举例,抽象为树形结构如图:
本题求子序列,很明显一个元素不能重复使用,所以需要startIndex,调整下一层递归的起始位置。
本题其实类似求子集问题,也是要遍历树形结构找每一个节点,所以和回溯算法:求子集问题!一样,可以不加终止条件,startIndex每次都会加1,并不会无限递归。
但本题收集结果有所不同,题目要求递增子序列大小至少为2,所以需要加个判断。
同时,同一父节点下的同层上使用过的元素就不能再使用了。所以单层中,我们使用了一个hashset,每次都add一下。但是后面,没有对应的remove操作!
这里是需要注意的地方,hashset是记录本层元素是否重复使用,新的一层set都会重新定义(清空),所以要知道set只负责本层!
class Solution {List<List<Integer>> result = new ArrayList<>();List<Integer> paths = new ArrayList<>();public List<List<Integer>> findSubsequences(int[] nums) {backtracking(nums,0);return result;}public void backtracking(int[] nums, int startIndex){if(paths.size() >= 2)result.add(new ArrayList<>(paths));HashSet<Integer>set = new HashSet<>();for(int i=startIndex; i < nums.length ; i++){if(paths.size() >0 && paths.get(paths.size() - 1) > nums[i] || set.contains(nums[i])){continue;}paths.add(nums[i]);set.add(nums[i]);backtracking(nums, i+1);paths.removeLast();}}
}
时间复杂度: O(n * 2^n)
空间复杂度: O(n)
46.全排列
题目链接:https://leetcode.cn/problems/permutations/
思路:
给定一个 没有重复 数字的序列,返回其所有可能的全排列。
此时我们已经学习了77.组合问题、 131.分割回文串 和78.子集问题 ,接下来看一看排列问题。
我以[1,2,3]为例,抽象成树形结构如下:
首先排列是有序的,也就是说 [1,2] 和 [2,1] 是两个集合,这和之前分析的子集以及组合所不同的地方。
可以看出元素1在[1,2]中已经使用过了,但是在[2,1]中还要在使用一次1,所以处理排列问题就不用使用startIndex了。
但排列问题需要一个used数组,标记已经选择的元素,如上图的橘黄色部分所示。
终止情况:当收集元素的数组path的大小达到和nums数组一样大的时候,说明找到了一个全排列,也表示到达了叶子节点。
而used数组,其实就是记录此时path里都有哪些元素使用了,一个排列里一个元素只能使用一次。
class Solution {List<List<Integer>> result = new ArrayList<>();List<Integer> paths = new ArrayList<>();boolean[] used;public List<List<Integer>> permute(int[] nums) {used = new boolean[nums.length];Arrays.fill(used,false);backTracking(nums);return result;}public void backTracking(int[] nums){if(paths.size() == nums.length){result.add(new ArrayList<>(paths));return ;}for(int i=0;i<nums.length;i++){if(used[i]) continue; // 如果使用过,就跳过used[i] = true;paths.add(nums[i]);backTracking(nums);paths.removeLast();used[i] = false;}}
}
时间复杂度: O(n!)
空间复杂度: O(n)
47.全排列 II
题目链接:https://leetcode.cn/problems/permutations-ii/
思路:
这道题目和46.全排列 的区别在与给定一个可包含重复数字的序列,要返回所有不重复的全排列。
这里又涉及到去重了。去重的套路和40.组合总和II 、90.子集II 里的组合问题和子集问题的去重套路一样。
同时,去重一定要对元素进行排序,这样我们才方便通过相邻的节点来判断是否重复使用了。
我以示例中的 [1,1,2]为例 (为了方便举例,已经排序)抽象为一棵树,去重过程如图:
图中我们对同一树层,前一位(也就是nums[i-1])如果使用过,那么就进行去重。
在46.全排列 中已经详细讲解了排列问题的写法,在40.组合总和II 、90.子集II 中详细讲解了去重的写法,所以代码也就很好写出来了。
class Solution {List<List<Integer>> result = new ArrayList<>();List<Integer> paths = new ArrayList<>();boolean[] used2;public List<List<Integer>> permuteUnique(int[] nums) {Arrays.sort(nums);used2 = new boolean[nums.length];Arrays.fill(used2,false);backtracking(nums,used2);return result;}public void backtracking(int[] nums, boolean[] used){if(paths.size() == nums.length){result.add(new ArrayList<>(paths));return ;}for(int i=0;i<nums.length;i++){if(used[i]) continue; // 如果使用过,就跳过// 如果在同一树层使用过,就跳过if(i>0 && nums[i] == nums[i-1] && used[i-1] == false){continue;}used[i] = true;paths.add(nums[i]);backtracking(nums,used);paths.removeLast();used[i] = false;}}
}
时间复杂度: O(n! * n)
空间复杂度: O(n)