代码随想录day25,回溯算法part4

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;
        }       
    }
}
相关推荐
黎雁·泠崖4 小时前
【魔法森林冒险】3/14 Allen类(一):主角核心属性与初始化
java·开发语言
黎雁·泠崖4 小时前
【魔法森林冒险】1/14 项目总览:用Java打造你的第一个回合制冒险游戏
java·开发语言
独好紫罗兰4 小时前
对python的再认识-基于数据结构进行-a006-元组-拓展
开发语言·数据结构·python
NuageL4 小时前
原始Json字符串转化为Java对象列表/把中文键名变成英文键名
java·spring boot·json
C++ 老炮儿的技术栈4 小时前
Qt 编写 TcpClient 程序 详细步骤
c语言·开发语言·数据库·c++·qt·算法
KYGALYX5 小时前
逻辑回归详解
算法·机器学习·逻辑回归
222you5 小时前
Redis的主从复制和哨兵机制
java·开发语言
铉铉这波能秀5 小时前
LeetCode Hot100数据结构背景知识之集合(Set)Python2026新版
数据结构·python·算法·leetcode·哈希算法
参.商.5 小时前
【Day 27】121.买卖股票的最佳时机 122.买卖股票的最佳时机II
leetcode·golang