leetcode354 - 俄罗斯套娃信封问题(二分查找+动态规划求最长递增子序列问题)
介绍
标签:二分查找,动态规划
354. 俄罗斯套娃信封问题
难度 困难
354. 俄罗斯套娃信封问题:
https://leetcode-cn.com/problems/russian-doll-envelopes/
题目
给定一些标记了宽度和高度的信封,宽度和高度以整数对形式(w, h)
出现。当另一个信封的宽度和高度都比这个信封大的时候,这个信封就可以放进另一个信封里,如同俄罗斯套娃一样。
请计算最多能有多少个信封能组成一组“俄罗斯套娃”信封(即可以把一个信封放到另一个信封里面)。
说明:
不允许旋转信封。
示例:
输入: envelopes = [[5,4],[6,4],[6,7],[2,3]]
输出: 3
解释: 最多信封的个数为 3, 组合为: [2,3] => [5,4] => [6,7]。
解题思路
分析题目意图
题目给出了一个二维数组,数字有大有小,最终的要求是使得(w,h)
两个数都是大数套小数,最终形成一个二维的最长递增子序列
对于单维的最长递增子序列,那么毫无疑问是用动态规划来计算是更合适的,但是对于二维的要怎么处理才是更好的呢?
- 直接排序,严格按照w与h都递增的关系排序
- w按照递增的关系排序,h按照递减的关系排序
然后通过排序后的数组,进行求最长递增子序列的运算
分析解题方法
第一个步骤:排序
排序有两种方案,其实两种方式本质都是一样的,只是第二种方式可以减少判断的次数,因此采用第二种
对于宽度 w 相同的数对,高度 h 降序排序。因为两个宽度相同的信封不能相互包含的,降序排序保证在 w 相同的数对中最多只选取一个
第二个步骤:求最长增长子序列
O(n^2)
也即是普通动态规划
每个信封都往前寻找它能装下的信封,然后获得他们的最大数量,然后加1
f(x) = f(x`) + 1(但是有判断条件)
for (int i = 1; i < n; ++i) {
for (int j = 0; j < i; ++j) {
// 根据高度进行判断
if (envelopes[j][1] < envelopes[i][1]) {
// i:小j啊?你能装几个了啊?
f[i] = Math.max(f[i], f[j] + 1);
}
}
// 更新res
res = Math.max(res, f[i]);
}
O(n * logn)
二分查找的方法求最长递增子序列
这个方法的核心是 LIS的长度等于划分序列的LDS的数量
简单来说就是求有几个不下降子序列就好了
详细证明搜 Dilworth定理
我是看的是视频什么是最长不下降子序列(LIS)?,大约从第10分钟开始,我觉得讲的很形象简单
一个求lis长度的模板
// 求lis的长度
public int lengthOfLIS(int[] nums) {
// piles记录划分序列的lds的数量
int piles = 0, n = nums.length;
int[] last = new int[n];
for (int i = 0; i < n; i++) {
// 要处理的那个数
int tmp = nums[i];
// 双指针指向首尾两个不下降子序列
int left = 0, right = piles;
// 二分查找应该放入哪个子序列
while (left < right) {
int mid = (left + right) / 2;
if (last[mid] >= tmp)
right = mid;
else
left = mid + 1;
}
// 哪里都放不进去,新建一条子序列
if (left == piles) piles++;
// 把当前数放到对应子序列的结尾,也就是更新last数组
last[left] = tmp;
}
return piles;
}
代码
方法一:一般动态规划
class Solution {
public int maxEnvelopes(int[][] envelopes) {
int n = envelopes.length;
if(n == 0) return 0;
// 按宽度升序排列,如果宽度一样,则按高度降序排列
Arrays.sort(envelopes, (int[] a, int[] b) -> {return a[0] == b[0] ? b[1] - a[1] : a[0] - b[0];});
// 普普通通动态规划
int[] f = new int[n];
Arrays.fill(f, 1);
int res = 1;
for (int i = 1; i < n; ++i) {
for (int j = 0; j < i; ++j) {
// 根据高度进行判断
if (envelopes[j][1] < envelopes[i][1]) {
// i:小j啊?你能装几个了啊?
f[i] = Math.max(f[i], f[j] + 1);
}
}
// 更新res
res = Math.max(res, f[i]);
}
return res;
}
}
效率:
方法二:二分查找+动态规划
class Solution {
public int maxEnvelopes(int[][] envelopes) {
int n = envelopes.length;
if(n == 0) return 0;
// 按宽度升序排列,如果宽度一样,则按高度降序排列
Arrays.sort(envelopes, (int[] a, int[] b) -> {return a[0] == b[0] ? b[1] - a[1] : a[0] - b[0];});
int piles = 0;
int[] last = new int[n];
for (int i = 0; i < n; i++) {
// 要处理的那个数
int tmp = envelopes[i][1];
// 双指针指向首尾两个不下降子序列
int left = 0, right = piles;
// 二分查找应该放入哪个子序列
while (left < right) {
int mid = (left + right) / 2;
if (last[mid] >= tmp)
right = mid;
else
left = mid + 1;
}
// 哪里都放不进去,新建一条子序列
if (left == piles) piles++;
// 把当前数放到对应子序列的结尾,也就是更新last数组
last[left] = tmp;
}
// 牌堆数就是 LIS 长度
return piles;
}
}
效率:
方法二的稍微优化
class Solution {
public int maxEnvelopes(int[][] envelopes) {
int n = envelopes.length;
if(n <= 1) return n;
Arrays.sort(envelopes, (int[] a, int[] b) -> {return a[0] == b[0] ? b[1] - a[1] : a[0] - b[0];});
int res= 0;
int[] last = new int[n];
last[0] = -1;
for (int i = 0; i < n; i++) {
int tmp = envelopes[i][1];
if(tmp > last[res]){
res++;
last[res] = tmp;
}
else{
int left = 0;
int right = res;
while(left < right){
int mid = (left + right) >>> 1;
if(last[mid] < tmp){
left = mid + 1;
}else{
right = mid;
}
}
last[left] = Math.min(last[left],tmp);
}
}
return res;
}
}
效率:
结语
看了一天的最长递增子序列的求法,终于看完了