LeetCode 491. 递增子序列

2 阅读4分钟

LeetCode 491. 递增子序列

摘要

LeetCode 491 递增子序列题解,采用回溯法在决策树上深度优先遍历,通过 used 集合在同一层去重(因数组未排序,不能简单跳过相邻重复),枚举所有长度至少为 2 的递增子序列,时间复杂度 O(n × 2^n),空间复杂度 O(n × 2^n)(结果存储)。

标签

#算法 #LeetCode #回溯 #DFS #数组 #哈希表 #题解

目录

  1. 题目描述
  2. 易错点
  3. 思路
  4. 编写代码

1. 题目描述

给你一个整数数组 nums,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。

数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。

示例 1:

text

输入: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]]

示例 2:

text

输入:nums = [4,4,3,2,1]
输出:[[4,4]]

提示:

  • 1 <= nums.length <= 15
  • -100 <= nums[i] <= 100

2. 易错点

  1. 不能排序:题目要求的是原数组的 子序列,即保持原数组的顺序。排序会改变顺序,从而破坏子序列的定义。
  2. 去重策略不同:由于不能排序,重复元素在数组中不一定相邻,因此无法像子集 II 那样通过 nums[i] == nums[i-1] 进行横向剪枝。必须使用哈希集合(Set)来记录当前层已选择的值。
  3. 横向去重 vs 纵向允许:同一层(同一递归深度)中,相同值只能选择一次,避免生成重复子序列;但不同层(纵向)允许出现相同值,例如 [7,7] 是合法的(两个 7 在不同深度)。
  4. 递增条件:当前选择的值必须大于等于 path 中最后一个值(因为相等也算递增)。
  5. 结果长度条件:只有当 path.length >= 2 时才记录当前路径为有效子序列。

3. 思路

决策树模型

本题的决策树根节点代表空路径。每个内部节点代表已选择的递增子序列前缀,树枝代表在当前 start 位置之后选择一个满足递增条件的元素。与之前题目不同的是,本题不能通过排序来简化去重,因为顺序必须保留。

为什么不能用排序去重?

在子集 II(LeetCode 90)中,我们通过排序让重复元素相邻,然后用 if (i > start && nums[i] == nums[i-1]) continue; 跳过同一层的重复选择。但本题要求子序列保持原数组顺序,排序会破坏顺序,因此必须换一种方法。

横向去重:使用 used 集合

在每一层递归中,我们需要确保 同一值 在当前 for 循环中只被选择一次。由于重复元素可能出现在数组的不同位置(不相邻),我们用一个 Set 记录当前层已经尝试过的值。当遍历到 nums[i] 时:

  • 若该值已经在 used 中,说明当前层已经尝试过这个值,继续选会产生重复子序列,因此跳过。
  • 否则,将该值加入 used,然后正常选择并递归。

为什么 used 只作用于当前层?
因为 used 是在递归函数内部定义的局部变量,每次递归调用都会创建一个新的空 Set,因此它只影响当前层的去重,不影响其他层(纵向)。这样,不同层可以出现相同的值,而同一层不会重复选择同一个值。

递增条件的检查

在尝试加入 nums[i] 前,需检查 path.length === 0 或 nums[i] >= path[path.length - 1]。若当前值小于路径末尾值,则跳过该分支(不满足递增)。

回溯过程

  1. 定义递归函数 dfs(start)

    • 若 path.length >= 2,将当前 path 的拷贝加入结果集。

    • 创建 used 集合。

    • 遍历 i 从 start 到 nums.length - 1

      • 若 path 非空且 nums[i] < path[path.length-1],跳过(不满足递增)。
      • 若 nums[i] 已在 used 中,跳过(同一层重复值去重)。
      • 否则,将 nums[i] 加入 used,再加入 path,递归 dfs(i+1),然后 path.pop() 回溯。
  2. 主函数初始化结果数组,调用 dfs(0),返回结果。

剪枝条件总结

  • 递增剪枝:保证当前值不小于路径末尾。
  • 横向去重剪枝:通过 used 集合避免同一层重复选择相同值。
  • 没有排序剪枝,因为顺序必须保留。

4. 编写代码

javascript 运行

javascript

var findSubsequences = function(nums) {
    const ans = [];
    const path = [];

    const dfs = (start) => {
        if (path.length >= 2) {
            ans.push([...path]);
        }

        const used = new Set();

        for (let i = start; i < nums.length; i++) {
            if (path.length > 0 && nums[i] < path[path.length - 1]) continue;
            if (used.has(nums[i])) continue;
            used.add(nums[i]);

            path.push(nums[i]);
            dfs(i + 1);
            path.pop();
        }
    };

    dfs(0);
    return ans;
};