代码随想录算法训练营第26天 | 题目:491.递增子序列 、46.全排列 、47.全排列 II 、(难题略读)
文章来源:代码随想录
题目名称:491.递增子序列
给定一个整型数组, 你的任务是找到所有该数组的递增子序列,递增子序列的长度至少是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]。
给定数组中可能包含重复数字,相等的数字应该被视为递增的一种情况
第一想法:
首先,本题不能有重复序列,取有序数组,相似于90题,但问题在于在90题中执行了排序操作,本题不能进行排序,所以需要新的去重方式。
使用回溯算法,先构思算法树状图。
每一层取一个元素,若所取元素小于子序列最后元素剪枝,若本层中已经取过相同值的元素剪枝。
解答思路:
回溯三部曲
1.递归函数参数
本题求子序列,很明显一个元素不能重复使用,所以需要startIndex,调整下一层递归的起始位置。
2.终止条件
本题其实类似求子集问题,也是要遍历树形结构找每一个节点,所以和回溯算法:求子集问题! (opens new window)一样,可以不加终止条件,startIndex每次都会加1,并不会无限递归。
但本题收集结果有所不同,题目要求递增子序列大小至少为2。
3.单层搜索逻辑
图中可以看出,同一父节点下的同层上使用过的元素就不能再使用了
困难:
使用hash表的map能取得更快的速度
//法二:使用map
class Solution {
//结果集合
List<List<Integer>> res = new ArrayList<>();
//路径集合
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> findSubsequences(int[] nums) {
getSubsequences(nums,0);
return res;
}
private void getSubsequences( int[] nums, int start ) {
if(path.size()>1 ){
res.add( new ArrayList<>(path) );
// 注意这里不要加return,要取树上的节点
}
HashMap<Integer,Integer> map = new HashMap<>();
for(int i=start ;i < nums.length ;i++){
if(!path.isEmpty() && nums[i]< path.getLast()){
continue;
}
// 使用过了当前数字
if ( map.getOrDefault( nums[i],0 ) >=1 ){
continue;
}
map.put(nums[i],map.getOrDefault( nums[i],0 )+1);
path.add( nums[i] );
getSubsequences( nums,i+1 );
path.removeLast();
}
}
}
题目名称:46.全排列
给定一个 没有重复 数字的序列,返回其所有可能的全排列。
示例:
输入: [1,2,3]
输出: [ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ]
#算法公开课
第一想法:
排列问题也是使用回溯算法模拟暴力求解,排列问题由于每次都要从头获取,所以不需要用startindex
对于不取取过的值,需要一个数组used来标记是否使用过。
解答思路:
1.递归函数参数
nums、used数组
2.终止
当path的长度等于nums的长度终止
3.单层
如果uesd[i]==1,continue。used[i]=1、path放入nums[i]、backtracking(nums, used)、弹出nums[i],used[i]=0;
题目名称:47.全排列 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
第一想法:
比较于上一题,本题需要进行去重操作,依据之前的方式,首先进行排序,当nums[i]=nums[i-1],如果used[i-1]为0,则说明这一个元素已经取过了。
解答思路:
图中我们对同一树层,前一位(也就是nums[i-1])如果使用过,那么就进行去重。
一般来说:组合问题和排列问题是在树形结构的叶子节点上收集结果,而子集问题就是取树上所有节点的结果。
在46.全排列 (opens new window)中已经详细讲解了排列问题的写法,在40.组合总和II (opens new window)、90.子集II (opens new window)中详细讲解了去重的写法,所以这次我就不用回溯三部曲分析了,直接给出代码,如下:
class Solution {
//存放结果
List<List<Integer>> result = new ArrayList<>();
//暂存结果
List<Integer> path = new ArrayList<>();
public List<List<Integer>> permuteUnique(int[] nums) {
boolean[] used = new boolean[nums.length];
Arrays.fill(used, false);
Arrays.sort(nums);
backTrack(nums, used);
return result;
}
private 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++) {
// 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;//标记同⼀树⽀nums[i]使⽤过,防止同一树枝重复使用
path.add(nums[i]);
backTrack(nums, used);
path.remove(path.size() - 1);//回溯,说明同⼀树层nums[i]使⽤过,防止下一树层重复
used[i] = false;//回溯
}
}
}
}
困难:
大家发现,去重最为关键的代码为:
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {
continue;
}
如果改成 used[i - 1] == true, 也是正确的!,去重代码如下:
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == true) {
continue;
}
这是为什么呢,就是上面我刚说的,如果要对树层中前一位去重,就用used[i - 1] == false,如果要对树枝前一位去重用used[i - 1] == true。
对于排列问题,树层上去重和树枝上去重,都是可以的,但是树层上去重效率更高!
用输入: [1,1,1] 来举一个例子。
树层上去重(used[i - 1] == false),的树形结构如下:
树枝上去重(used[i - 1] == true)的树型结构如下:
应该很清晰的看到,树层上对前一位去重非常彻底,效率很高,树枝上对前一位去重虽然最后可以得到答案,但是做了很多无用搜索。
收获:
树层去重相较于树枝去重更有效,回溯时的 path.remove(path.size() - 1);是因为数组有0的问题