开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第28天,点击查看活动详情
354. 俄罗斯套娃信封问题 题目描述:给你一个二维整数数组 envelopes ,其中 ,表示第 i 个信封的宽度和高度。当另一个信封的宽度和高度都比这个信封大的时候,这个信封就可以放进另一个信封里,如同俄罗斯套娃一样。
请计算 最多能有多少个 信封能组成一组“俄罗斯套娃”信封(即可以把一个信封放到另一个信封里面)。
| 示例1 | 示例2 |
|---|---|
输入:envelopes = [[5,4],[6,4],[6,7],[2,3]] 输出: 解释:最多信封的个数为 3, 组合为: [2,3] => [5,4] => [6,7]。 | 输入:envelopes = [[1,1],[1,1],[1,1]] 输出: |
中规中矩的动态规划 (❌ 可惜超时了)
该题目其实是 # 300. 最长递增子序列 的升级版。首先我们要对数组进行整理:先按照 升序排列,如果 相等,再按照 降序排列。之后再对 使用最长递增子序列的动态规划算法。
1、确定 dp 状态数组
首先记 为数组 按照 升序排列,如果 相等,再按照 降序排列后的数组;再定义 为以 结尾的严格递增子序列的长度,其中 。
2、确定 dp 状态方程
既然子序列可以不连续,当我们遍历到 时,不能仅仅判断 与 ,还额外需要另外一维状态,即 ,其中 。
-
当 时,符合严格递增, 此时存在 ;
-
当 时,直接忽略即可。
NOTE: 每次遍历 时需要对 取最大值,即
3、确定 dp 初始状态
初始化的每个元素为 (非严格增序数列的最长递增序列为 );
4、确定遍历顺序
-
外层循环从 遍历到 ;
-
内层循环从 遍历到 。
5、确定最终返回值
回归到 定义中, 仅代表以 结尾的最长子序列的长度,并不是全局最长子序列的长度,应该从 数组中选择最大值,即 。
6、代码示例
/**
* ❌ 超时了
* 空间复杂度 O(n)
* 时间复杂度 O(n^2)
*/
function maxEnvelopes(envelopes: number[][]): number {
envelopes.sort((a, b) => a[0] === b[0] ? b[1] - a[1] : a[0] - b[0]); // O(nlogn)
const n = envelopes.length;
const dp = new Array(n).fill(1);
for (let i = 1; i < n; i++) {
for (let j = 0; j < i; j++) {
if (envelopes[i][1] > envelopes[j][1]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
}
return Math.max(...dp);
};
基于二分查找的算法
没想到本题用中规中矩的动态规划竟然会判超时,既然时间复杂度 会超时,自然会想到有没有 的算法?logn?这里会用二分法? 举例说明,把如下的牌序
按照规则进行分落:只能把点数小的牌压到点数比它大的牌上;如果当前牌点数较大没有可以放置的堆,则新建一个堆,把这张牌放进去;如果当前牌有多个堆可供选择,则选择最左边的那一堆放置。
这样处理完后,牌落顶部(top cards)为有序数列(能用二分法),而牌落数就是最长递增子序列的长度。
1、初始化Top Cards数组和牌落数
初始化 top 数组的长度为原数组 nums 的长度(因为,该数组的长度不可知,但是不会超过原数组 nums 的长度);
牌落数初始化为 。
2、二分搜索模型
这里选择 左闭右开 区间模型(二分法规则可参考:# 重识二分法(上)、# 重识二分法(中)、# 重识二分法(下)),初始 、,循环条件为 , 指针计算为 。
-
如果 , 指针不动, 后继续二分;
-
如果 , 指针不动, 后继续二分;
其中 是当前 数组正在循环遍历的元素。
3、牌落数增加
当没找到合适的牌落,才会新建一落,那么此时的判断条件是 ,同时替换牌落顶部的元素 。
4、代码示例
/**
* 空间复杂度 O(n) n是envelopes数组的长度
* 时间复杂度 O(nlogn)
*/
function maxEnvelopes(envelopes: number[][]): number {
const heights = envelopes
.sort((a, b) => a[0] === b[0] ? b[1] - a[1] : a[0] - b[0])
.map(e => e[1]);
return lengthOfLIS(heights);
};
/**
* 空间复杂度 O(n) n是nums数组的长度
* 时间复杂度 O(nlogn)
*/
function lengthOfLIS(nums: number[]) {
const top = new Array(nums.length);
let n = 0;
for (const target of nums) {
let left = 0; let right = n;
while (left < right) {
const mid = Math.floor((left + right) / 2);
if (top[mid] >= target) {
right = mid;
} else {
left = mid + 1;
}
}
if (left === n) {
n += 1;
}
top[left] = target;
}
return n;
}