491. 非递减子序列(Non-decreasing Subsequences)

频次 ★★★ · 难度 🟡 · 高频:字节

题目

给定一个整数数组 nums,找出所有不同的递增子序列(长度至少为 2)。数组中可能含有重复元素,子序列中相邻元素非严格递增(即 nums[i] <= nums[j])。

注意:不能对数组排序,因为求的是子序列(保持原顺序),不是子集。

示例

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

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

思路

回溯 + 每层 HashSet 去重:因为不能排序(子序列需保持原顺序),不能用排序 + nums[i] == nums[i-1] 的去重法。改为每层递归用一个 HashSet 记录本层已选过的数字,nums[i] 已在本层出现过则跳过。

剪枝:当前数字 < 路径最后一个数字时跳过(保证非递减)。

代码

public List<List<Integer>> findSubsequences(int[] nums) {
    List<List<Integer>> res = new ArrayList<>();
    backtrack(nums, 0, new ArrayList<>(), res);
    return res;
}
 
private void backtrack(int[] nums, int start, List<Integer> path, List<List<Integer>> res) {
    if (path.size() >= 2) {
        res.add(new ArrayList<>(path));
        // 注意:这里不能 return,因为还要继续往后找更长的子序列
    }
    Set<Integer> used = new HashSet<>();       // 本层去重(不能排序,只能用 set)
    for (int i = start; i < nums.length; i++) {
        if (used.contains(nums[i])) continue;  // 本层已用过该值
        if (!path.isEmpty() && nums[i] < path.get(path.size() - 1)) continue; // 非递减
        used.add(nums[i]);
        path.add(nums[i]);
        backtrack(nums, i + 1, path, res);
        path.remove(path.size() - 1);
    }
}

复杂度

  • 时间:O(2^n × n) —— 每个元素选或不选,最坏生成所有子序列
  • 空间:O(n) —— 递归栈深度 + path

边界条件

  • 数组长度 < 2:无法形成长度 ≥ 2 的子序列,返回空列表
  • 全部递减数组(如 [5,4,3,2,1]):无递增子序列,返回空列表
  • 全相同元素(如 [7,7,7,7]):每层 set 去重 + 直接收集,子序列数量 = 2^n - n - 1(所有长度 ≥ 2 的组合)

变式

易错点

  • 不能排序:题目求的是子序列(保持原顺序),排序后变成子集问题,会生成错误结果(如 [4,3,2,1] 排序后变成 [1,2,3,4],会错误地生成 [1,2] 等)
  • 收集后不能 return:path.size() >= 2 时收集结果,但不能 return,因为后续还能接更多数字形成更长序列
  • 去重用 HashSet 而非排序法i > start && nums[i] == nums[i-1] 依赖排序,本题不能排序,必须在每层用 set 去重

面试追问

  • 为什么不能用排序去重法? 排序去重法(nums[i] == nums[i-1])的前提是数组已排序,但本题求的是子序列,排序会破坏原数组顺序,导致生成的结果不是原数组的子序列
  • 每层 set 去重的原理? 在同一层递归中,相同值的元素只需选第一个(最靠左的),后面的相同值会生成重复子序列,set 记录本层已选值来跳过

关联题

  • 同套路:90. 子集 II —— 排序去重法(对比学习)
  • 进阶:47. 全排列 II —— 排列去重(used[] 数组法)
  • 知识点:回溯去重三种方法见回溯