LeetCode 491. 递增子序列
摘要
LeetCode 491 递增子序列题解,采用回溯法在决策树上深度优先遍历,通过 used 集合在同一层去重(因数组未排序,不能简单跳过相邻重复),枚举所有长度至少为 2 的递增子序列,时间复杂度 O(n × 2^n),空间复杂度 O(n × 2^n)(结果存储)。
标签
#算法 #LeetCode #回溯 #DFS #数组 #哈希表 #题解
目录
- 题目描述
- 易错点
- 思路
- 编写代码
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. 易错点
- 不能排序:题目要求的是原数组的 子序列,即保持原数组的顺序。排序会改变顺序,从而破坏子序列的定义。
- 去重策略不同:由于不能排序,重复元素在数组中不一定相邻,因此无法像子集 II 那样通过
nums[i] == nums[i-1]进行横向剪枝。必须使用哈希集合(Set)来记录当前层已选择的值。 - 横向去重 vs 纵向允许:同一层(同一递归深度)中,相同值只能选择一次,避免生成重复子序列;但不同层(纵向)允许出现相同值,例如
[7,7]是合法的(两个 7 在不同深度)。 - 递增条件:当前选择的值必须大于等于
path中最后一个值(因为相等也算递增)。 - 结果长度条件:只有当
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]。若当前值小于路径末尾值,则跳过该分支(不满足递增)。
回溯过程
-
定义递归函数
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()回溯。
- 若
-
-
主函数初始化结果数组,调用
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;
};