一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第3天,点击查看活动详情。
题目描述
给你一个二维整数数组 envelopes ,其中 envelopes[i] = [wi, hi] ,表示第 i 个信封的宽度和高度。
当另一个信封的宽度和高度都比这个信封大的时候,这个信封就可以放进另一个信封里,如同俄罗斯套娃一样。
请计算 最多能有多少个 信封能组成一组“俄罗斯套娃”信封(即可以把一个信封放到另一个信封里面)。
注意:不允许旋转信封。
示例 1:
输入: envelopes = [[5,4],[6,4],[6,7],[2,3]]
输出: 3
解释: 最多信封的个数为 3, 组合为: [2,3] => [5,4] => [6,7]。
示例 2:
输入: envelopes = [[1,1],[1,1],[1,1]]
输出: 1
提示:
1 <= envelopes.length <= 105envelopes[i].length == 21 <= wi, hi <= 105
解题思路
- 首先需要对给定的数组进行排序,宽度按照正序排列,如果宽度相等,则按照高度逆序排列。
- 维护一个存储严格递增高度的数组
为什么需要这么个数组呢? 我们不妨看个用例:
[[2,3], [3,6], [3,5], [3.4], [4,5], [5,6]]如果我们把[3,6]加入到队列中,那么[4,5], [5,6]对被排除在了队列之外(得一失二显然违背了我们的意愿); 所以正确的结果应该是舍去[3,6],取[4,5], [5,6]入队列。
那么算法上应该如何表示?
其实就是我们上面提到的有序数组,当[3,6]的6入数组之后,其后的5和4会依次去替换掉它的位置,这样就能保证[4,5], [5,6]能够被推入到队列中。
这也就解释了为什么我们需要首先将原始数组进行宽度按照正序排列,如果宽度相等,则按照高度逆序排列这样的排序了。
还是不明白?
换个思路,其实我们要找的是在某个元素之前宽高都小于这个元素的 元素的个数(宽度相等的元素理解为一个元素,因为最多只会有一个满足题意)。
这就是动态规划的核心思想!
因为我们按照宽度排序已经确保了宽度按照升序排列,所以只要是高度大于前者,都可以推入队列。
当后一个元素的高小于数组的最后一个值,并大于倒数第二个值(数组长度为1时除外),那么直接覆盖最后一个值,实际的意义就是舍弃(覆盖)掉队列的最后一项。
- 数组的长度即为最长有序队列的长度。 但其实返回的数组并不是真正有序队列的高度哦!
比如下面这个用例:
[[5,8], [6,9], [7, 7], [9, 10]] 返回 [7, 9, 10]
但实际的队列却是 [[5,8], [6,9], [9,10]]
代码
/*
* @lc app=leetcode.cn id=354 lang=javascript
*
* [354] 俄罗斯套娃信封问题
*/
// @lc code=start
/**
* @param {number[][]} envelopes
* @return {number}
*/
// 动态规划法 + 二分法
var maxEnvelopes = function (envelopes) {
// 按照宽度正序,高度逆序排列
envelopes.sort((a, b) => a[0] - b[0] || b[1] - a[1]);
// console.log('sort', envelopes);
const f = [envelopes[0][1]]; // 存储递增的h列表
for (let i = 1; i < envelopes.length; ++i) {
const num = envelopes[i][1];
if (num > f.at(-1)) {
f.push(num);
} else {
// 二分查找,为num在f中锚定一个位置,并替换掉该位置上的数字
const index = binarySearch(f, num);
f[index] = num;
}
}
// console.log("f", f);
return f.length;
};
function binarySearch(list, target) {
let low = 0,
high = list.length - 1;
while (low < high) {
const mid = Math.floor((high - low) / 2) + low;
if (list[mid] < target) {
low = mid + 1;
} else {
high = mid;
}
}
return low;
}
// @lc code=end