491.递增子序列
题目
[力扣题目链接](https://leetcode.cn/problems/non-decreasing-subsequences/)
给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。
数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。
示例 1:
输入:nums = [4,6,7,7]
输出:[[4,6],[4,6,7],[4,6,7,7],[4,7],[4,7,7],[6,7],[6,7,7],[7,7]]
思路
和90.子集II可重不可复选有点像,但是有区别。
-
子集要求是所有子集,而这道题是递增的子序列。
-
子集无顺序要求,可以打乱排序后处理用于去重。而本题对于输入的顺序不能打乱排序,因为会破坏原顺序。假如输入的[1, 2, 3, 1, 1],原序列递增子序列:[1,2,3], [1,2], [1,3], [2,3], [1,1,1]等,排序后:[1,1,1,2,3]会得到 [1,1,2], [1,1,3] 等,但原序列中这些1不相邻,不能随意组合!
-
去重手段子集是排序+剪枝,去重位置是i > start && nums[i] == nums[i-1]。本题是用哈希表或者数组去记录,去重位置是used[nums[i] + 100]`本层已用
两个去重的示例
90题(排序后):[1,2,2,2] start=0 ├── i=0, 选1 ├── i=1, 选2(第1个) ← 本层第一个2,可用 ├── i=2, 选2(第2个) ← i>start && nums[2]==nums[1],跳过! └── i=3, 选2(第3个) ← i>start && nums[3]==nums[2],跳过!
依赖排序后相邻元素相等来判断
491题(不能排序):[4, 6, 7, 7] start=0, used=[F,F,F...] ├── i=0, 选4, used[4]=T ├── i=1, 选6, used[6]=T ├── i=2, 选7, used[7]=T └── i=3, 选7, used[7]=T?→ 已经是T了,跳过!✓
不依赖排序,用哈希表记录本层出现过的数字
根据这些区别去更改90题的代码,注意下面代码的区别
代码
java
class Solution {
List<List<Integer>> res = new LinkedList<>();
LinkedList<Integer> track = new LinkedList<>();
public List<List<Integer>> findSubsequences(int[] nums) {
backtrack(nums, 0);//不要排序
return res;
}
void backtrack(int[] nums, int start) {
// 收集条件:长度至少为2,子集没有长度要求
if (track.size() >= 2) {
res.add(new LinkedList<>(track));
}
//关键:用哈希表记录本层已用过的数字,避免重复
// 数组优化:nums[i] 范围 [-100, 100],+100映射到[0,200],每层新建,天然隔离
boolean[] used = new boolean[201];
for (int i = start; i < nums.length; i++) {
//剪枝1:不是递增,跳过
if (!track.isEmpty() && nums[i] < track.getLast()) continue;
//剪枝2:本层已用过这个数,跳过(去重核心)
if (used[nums[i] + 100]) continue;
// 做选择
used[nums[i] + 100] = true; //标记本层已用
track.addLast(nums[i]);
backtrack(nums, i + 1);
track.removeLast();
// 注意:不需要撤销used标记!因为used是本层循环的局部状态
}
}
}
46.全排列
题目
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例 1:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
思路
这是无重不可复选的排列。
组合/子集问题,使用 start 变量保证元素 nums[start] 之后只会出现 nums[start+1..] 中的元素,通过固定元素的相对位置保证不出现重复的子集。
但排列问题本身就是让你穷举元素的位置,nums[i] 之后也可以出现 nums[i] 左边的元素,所以需要额外使用 used 数组来标记哪些元素还可以被选择。
我们用 used 数组标记已经在路径上的元素避免重复选择,然后收集所有叶子节点上的值,就是所有全排列的结果.
代码
完整的每一句的注释在回溯算法哪里有些,这里的代码注释主要写和上面组合子集代码的区别:
java
class Solution{
List<List<Integer>> res = new LinkedList<>();
LinkedList<Integer> track = new LinkedList<>();
//多一个这个,判断是否已经选择
boolean[] used;
// 主函数,输入一组不重复的数字,返回它们的全排列
public List<List<Integer>> permute(int[] nums){
//主函数里也增加这个used去判断
used = new boolean[nums.length];
backtrack(nums);
return res;
}
// 回溯算法核心函数
void backtrack(int[] nums){
//这里和组合一样,有一个if判断什么时候该记录结果
if(track.size() == nums.length){
res.add(new LinkedList(track));
return;
}
// 回溯算法标准框架
for(int i = 0;i<nums.length;i++){
//这里对应多了判断是否已经添加,已经存在 track 中的元素,不能重复选择
if(used[i]){
continue;
}
//添加前变成是
used[i] = true;
track.addLast(nums[i]);
//注意输入要一致
backtrack(nums);
track.removeLast();
//移除后变成否,让它可以被后续其他分支重新选择
used[i]=false;
}
}
}
47.全排列 II
题目
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
示例 1:
输入:nums = [1,1,2]
输出:
[[1,1,2],
[1,2,1],
[2,1,1]]
思路
这是无重可复选的排列,对比之前的标准全排列解法代码,两处不同:
1、对 nums 进行了排序。
2、添加了一句额外的剪枝逻辑。
但是注意排列问题的剪枝逻辑,和子集/组合问题的剪枝逻辑略有不同:新增了 !used[i - 1] 的逻辑判断。
假设输入为 nums = [1,2,2'],标准的全排列算法会得出如下答案:
[
[1,2,2'],[1,2',2],
[2,1,2'],[2,2',1],
[2',1,2],[2',2,1]
]
显然,这个结果存在重复,比如 [1,2,2'] 和 [1,2',2] 应该只被算作同一个排列,但被算作了两个不同的排列。所以现在的关键在于,保证相同元素在排列中的相对位置保持不变。
比如说 nums = [1,2,2'] 这个例子,保持排列中 2 一直在 2' 前面。这样的话,从上面 6 个排列中只能挑出 3 个排列符合这个条件:
[ [1,2,2'],[2,1,2'],[2,2',1] ]
进一步,如果再增加一个重复元素, nums = [1,2,2',2''],只要保证重复元素 2 的相对位置固定,比如说 2 -> 2' -> 2'',也可以得到无重复的全排列结果。
因为标准全排列算法之所以出现重复,是因为把相同元素形成的排列序列视为不同的序列,但实际上它们应该是相同的;而如果固定相同元素形成的序列顺序,当然就避免了重复。
那么反映到代码上,注意看这个剪枝逻辑:
java
// 新添加的剪枝逻辑,固定相同的元素在排列中的相对位置
//used[i - 1] == false(即 !used[i-1]):表示「前一个重复元素还没被选」;此时若选择当前元素 nums[i],就会出现 "跳过前一个重复元素、直接选后一个" 的情况(违背 "按顺序选" 的规则)
if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) {
// 如果前面的相邻相等元素没有用过,则跳过
continue;
}
// 选择 nums[i]
当出现重复元素时,比如输入 nums = [1,2,2',2''],2' 只有在 2 已经被使用的情况下才会被选择,同理,2'' 只有在 2' 已经被使用的情况下才会被选择,这就保证了相同元素在排列中的相对位置保证固定。
代码
java
class Solution{
List<List<Integer>> res = new LinkedList<>();
LinkedList<Integer> track = new LinkedList<>();
boolean[] used;
// 主函数,输入一组不重复的数字,返回它们的全排列
public List<List<Integer>> permuteUnique(int[] nums){
// 新增先排序,让相同的元素靠在一起,有重复就需要先排序
Arrays.sort(nums);//新增
used = new boolean[nums.length];
backtrack(nums);
return res;
}
// 回溯算法核心函数
void backtrack(int[] nums){
if(track.size() == nums.length){
res.add(new LinkedList(track));
return;
}
// 回溯算法标准框架
for(int i = 0;i<nums.length;i++){
if(used[i]){
continue;
}
//同样重复需要剪枝,新添加的剪枝逻辑,固定相同的元素在排列中的相对位置
//used[i - 1] == false(即 !used[i-1])
if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) {
continue;
}
used[i] = true;
track.add(nums[i]);
backtrack(nums);
track.removeLast();
used[i]=false;
}
}
}