Day29
递增子序列
题目
给定一个整型数组, 你的任务是找到所有该数组的递增子序列,递增子序列的长度至少是2。
示例:
- 输入: [4, 6, 7, 7]
- 输出: [[4, 6], [4, 7], [4, 6, 7], [4, 6, 7, 7], [6, 7], [6, 7, 7], [7,7], [4,7,7]]
说明:
- 给定数组的长度不会超过15。
- 数组中的整数范围是 [-100,100]。
- 给定数组中可能包含重复数字,相等的数字应该被视为递增的一种情况。
思路
本题求自增子序列,是不能对原数组进行排序的,排完序的数组都是自增子序列了。
所以不能使用之前的去重逻辑!
回溯三部曲
- 递归函数参数
本题求子序列,很明显一个元素不能重复使用,所以需要startIndex,调整下一层递归的起始位置。
List<List<Integer>> res = new ArrayList<>();
List<Integer> paths = new ArrayList<>();
public void backtracking(int[] nums, int startIndex)
- 终止条件
本题其实类似求子集问题,也是要遍历树形结构找每一个节点,所以和回溯算法:求子集问题! (opens new window)一样,可以不加终止条件,startIndex每次都会加1,并不会无限递归。
但本题收集结果有所不同,题目要求递增子序列大小至少为2,所以代码如下:
if(startIndex > nums.length) return;
if(paths.size() > 1) {
res.add(new ArrayList<>(paths));
// 注意这里不要加return,因为要取树上的所有节点
}
- 单层搜索逻辑
同一父节点下的同层上使用过的元素就不能再使用了
那么单层搜索代码如下:
HashSet<Integer> set = new HashSet<>(); // 使用set来对本层元素进行去重
for(int i = startIndex; i < nums.length; i++){
// 因为要递增序列,所以可以用 paths.get(path.size() -1 ) > nums[i] 剪枝
if(!paths.isEmpty() && paths.get(paths.size() -1 ) > nums[i] || set.contains(nums[i])){
continue;
}
set.add(nums[i]);
paths.add(nums[i]);
backtracking(nums, i + 1);
paths.remove(paths.size() - 1);
}
这也是需要注意的点,`HashSet<Integer> set` 是记录本层元素是否重复使用,新的一层uset都会重新定义(清空),所以要知道uset只负责本层!
代码
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> paths = new ArrayList<>();
public List<List<Integer>> findSubsequences(int[] nums) {
backtracking(nums, 0);
return res;
}
public void backtracking(int[] nums, int startIndex){
if(startIndex > nums.length) return;
if(paths.size() > 1) res.add(new ArrayList<>(paths));
HashSet<Integer> set = new HashSet<>(); // 使用set来对本层元素进行去重
for(int i = startIndex; i < nums.length; i++){
// 因为要递增序列,所以可以用 paths.get(path.size() -1 ) > nums[i] 剪枝
if(!paths.isEmpty() && paths.get(paths.size() -1 ) > nums[i] || set.contains(nums[i])){
continue;
}
set.add(nums[i]);
paths.add(nums[i]);
backtracking(nums, i + 1);
paths.remove(paths.size() - 1);
}
}
}
class Solution {
private List<Integer> path = new ArrayList<>();
private List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> findSubsequences(int[] nums) {
backtracking(nums,0);
return res;
}
private void backtracking (int[] nums, int start) {
if (path.size() > 1) {
res.add(new ArrayList<>(path));
}
int[] used = new int[201];
for (int i = start; i < nums.length; i++) {
if (!path.isEmpty() && nums[i] < path.get(path.size() - 1) ||
(used[nums[i] + 100] == 1)) continue;
used[nums[i] + 100] = 1;
path.add(nums[i]);
backtracking(nums, i + 1);
path.remove(path.size() - 1);
}
}
}
全排列
题目
给定一个 没有重复 数字的序列,返回其所有可能的全排列。
示例:
- 输入: [1,2,3]
- 输出: [ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ]
思路
我以[1,2,3]为例,抽象成树形结构如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3dXkPZxr-1689341599420)(https://code-thinking-1253855093.file.myqcloud.com/pics/20211027181706.png “46.全排列”)]
- 递归函数参数
首先排列是有序的,也就是说 [1,2] 和 [2,1] 是两个集合,这和之前分析的子集以及组合所不同的地方。
可以看出元素1在[1,2]中已经使用过了,但是在[2,1]中还要在使用一次1,所以处理排列问题就不用使用startIndex了。
但排列问题需要一个used数组,标记已经选择的元素,如图橘黄色部分所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1El6aCRz-1689341599421)(https://code-thinking-1253855093.file.myqcloud.com/pics/20211027181706.png “46.全排列”)]
List<List<Integer>> res = new ArrayList<>();
List<Integer> paths = new ArrayList<>();
public void backtracking(int[] nums, boolean used[])
- 递归终止条件
当收集元素的数组path的大小达到和nums数组一样大的时候,说明找到了一个全排列,也表示到达了叶子节点。
if(paths.size() == nums.length){
res.add(new ArrayList<>(paths));
return;
}
- 单层搜索的逻辑
这里和77.组合问题 (opens new window)、131.切割问题 (opens new window)和78.子集问题 (opens new window)最大的不同就是for循环里不用startIndex了。
因为排列问题,每次都要从头开始搜索,例如元素1在[1,2]中已经使用过了,但是在[2,1]中还要再使用一次1。
而used数组,其实就是记录此时path里都有哪些元素使用了,一个排列里一个元素只能使用一次。
for(int i = 0; i < nums.length; i++){
if(used[i] == true) continue; // path里已经收录的元素,直接跳过
used[i] = true;
paths.add(nums[i]);
backtracking(nums, used);
paths.remove(paths.size() - 1);
used[i] = false;
}
代码
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> paths = new ArrayList<>();
public List<List<Integer>> permute(int[] nums) {
boolean used[] = new boolean[nums.length];
Arrays.fill(used, false);
backtracking(nums, used);
return res;
}
public void backtracking(int[] nums, boolean used[]){
if(paths.size() == nums.length){
res.add(new ArrayList<>(paths));
return;
}
for(int i = 0; i < nums.length; i++){
if(used[i] == true) continue;
used[i] = true;
paths.add(nums[i]);
backtracking(nums, used);
paths.remove(paths.size() - 1);
used[i] = false;
}
}
}
全排列 II
题目
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
示例 1:
- 输入:nums = [1,1,2]
- 输出: [[1,1,2], [1,2,1], [2,1,1]]
示例 2:
- 输入:nums = [1,2,3]
- 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
提示:
- 1 <= nums.length <= 8
- -10 <= nums[i] <= 10
思路
这道题目和46.全排列 (opens new window)的区别在与给定一个可包含重复数字的序列,要返回所有不重复的全排列。
这里又涉及到去重了。
在40.组合总和II (opens new window)、90.子集II (opens new window)我们分别详细讲解了组合问题和子集问题如何去重。
那么排列问题其实也是一样的套路。
还要强调的是去重一定要对元素进行排序,这样我们才方便通过相邻的节点来判断是否重复使用了。
我以示例中的 [1,1,2]为例 (为了方便举例,已经排序)抽象为一棵树,去重过程如图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iJ5dXMwu-1689341599421)(https://code-thinking-1253855093.file.myqcloud.com/pics/20201124201331223.png “47.全排列II1”)]
图中我们对同一树层,前一位(也就是nums[i-1])如果使用过,那么就进行去重。
一般来说:组合问题和排列问题是在树形结构的叶子节点上收集结果,而子集问题就是取树上所有节点的结果。
在46.全排列 (opens new window)中已经详细讲解了排列问题的写法,在40.组合总和II (opens new window)、90.子集II (opens new window)中详细讲解了去重的写法
代码
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> paths = new ArrayList<>();
public List<List<Integer>> permuteUnique(int[] nums) {
boolean used[] = new boolean[nums.length];
Arrays.sort(nums);
backtracking(nums, used);
return res;
}
private void backtracking(int[] nums, boolean used[]){
if(paths.size() == nums.length){
res.add(new ArrayList<>(paths));
return;
}
for(int i = 0; i < nums.length; i++){
// used[i - 1] == true,说明同⼀树⽀nums[i - 1]使⽤过
// used[i - 1] == false,说明同⼀树层nums[i - 1]使⽤过
// 如果同⼀树层nums[i - 1]使⽤过则直接跳过
if(i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false){
continue;
}
//如果同⼀树⽀nums[i]没使⽤过开始处理
if(used[i] == false){
used[i] = true;
paths.add(nums[i]);
backtracking(nums, used);
paths.remove(paths.size() - 1);
used[i] = false;
}
}
}
}