354. 俄罗斯套娃信封问题(russian doll envelopes)

3,652 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第28天,点击查看活动详情

354. 俄罗斯套娃信封问题 题目描述:给你一个二维整数数组 envelopes ,其中 envelopes[i]=[wi,hi]envelopes[i] = [w_i, h_i] ,表示第 i 个信封的宽度和高度。当另一个信封的宽度和高度都比这个信封大的时候,这个信封就可以放进另一个信封里,如同俄罗斯套娃一样。

请计算 最多能有多少个 信封能组成一组“俄罗斯套娃”信封(即可以把一个信封放到另一个信封里面)。

示例1示例2
输入envelopes = [[5,4],[6,4],[6,7],[2,3]]
输出33
解释:最多信封的个数为 3, 组合为: [2,3] => [5,4] => [6,7]
输入envelopes = [[1,1],[1,1],[1,1]]
输出11

中规中矩的动态规划 (❌ 可惜超时了)

该题目其实是 # 300. 最长递增子序列 的升级版。首先我们要对数组进行整理:先按照 wiw_i 升序排列,如果 wiw_i 相等,再按照 hih_i 降序排列。之后再对 hih_i 使用最长递增子序列的动态规划算法。

1、确定 dp 状态数组

首先记 envelopesenvelopes' 为数组 envelopesenvelopes 按照 wiw_i 升序排列,如果 wiw_i 相等,再按照 hih_i 降序排列后的数组;再定义 dp[i]dp[i] 为以 envelopes[i][1]envelopes'[i][1] 结尾的严格递增子序列的长度,其中 i[0,n),n=envelopes.lengthi \in [0,n),n=envelopes'.length

2、确定 dp 状态方程

既然子序列可以不连续,当我们遍历到 envelopes[i][1]envelopes'[i][1] 时,不能仅仅判断 envelopes[i][1]envelopes'[i][1]envelopes[i1][1]envelopes'[i - 1][1],还额外需要另外一维状态,即 envelopes[j][1]envelopes'[j][1],其中 j[0,i)j \in[0, i)

  • envelopes[i][1]>envelopes[j][1]envelopes'[i][1] \gt envelopes'[j][1] 时,符合严格递增, 此时存在 dp[i]=dp[j]+1dp[i] = dp[j] + 1

  • envelopes[i][1]envelopes[j][1]envelopes'[i][1] \le envelopes'[j][1] 时,直接忽略即可。

NOTE: 每次遍历 jj 时需要对 dp[i]dp[i] 取最大值,即 dp[i]=max(dp[i],dp[j]+1)dp[i] = max(dp[i], dp[j] + 1)

3、确定 dp 初始状态

dpdp 初始化的每个元素为 11(非严格增序数列的最长递增序列为 11);

4、确定遍历顺序

  • 外层循环从 i=1i = 1 遍历到 i=n1i = n - 1

  • 内层循环从 j=0j = 0 遍历到 j=i1j = i - 1

5、确定最终返回值

回归到 dpdp 定义中,dp[n1]dp[n - 1] 仅代表以 envelopes[n1][1]envelopes'[n-1][1] 结尾的最长子序列的长度,并不是全局最长子序列的长度,应该从 dpdp 数组中选择最大值,即 max(...dp)max(...dp)

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);
};

基于二分查找的算法

没想到本题用中规中矩的动态规划竟然会判超时,既然时间复杂度 O(n2)O(n^2) 会超时,自然会想到有没有 O(nlogn)O(nlogn) 的算法?logn?这里会用二分法? 举例说明,把如下的牌序

按照规则进行分落:只能把点数小的牌压到点数比它大的牌上;如果当前牌点数较大没有可以放置的堆,则新建一个堆,把这张牌放进去;如果当前牌有多个堆可供选择,则选择最左边的那一堆放置。

这样处理完后,牌落顶部(top cards)为有序数列(能用二分法),而牌落数就是最长递增子序列的长度。

1、初始化Top Cards数组和牌落数

初始化 top 数组的长度为原数组 nums 的长度(因为,该数组的长度不可知,但是不会超过原数组 nums 的长度);

牌落数初始化为 n=0n = 0

2、二分搜索模型

这里选择 左闭右开 [left,right)[left,right) 区间模型(二分法规则可参考:# 重识二分法(上)# 重识二分法(中)# 重识二分法(下)),初始 left=0left=0right=nright = n,循环条件为 left<rightleft<rightmidmid 指针计算为 mid=(left+right)/2mid=(left+right)/2

  • 如果 top[mid]>=targettop[mid] >= targetleftleft 指针不动,right=midright = mid 后继续二分;

  • 如果 top[mid]<targettop[mid] < targetrightright 指针不动,left=mid+1left = mid + 1 后继续二分;

其中 targettarget 是当前 numsnums 数组正在循环遍历的元素。

3、牌落数增加

当没找到合适的牌落,才会新建一落,那么此时的判断条件是 left===nleft === n,同时替换牌落顶部的元素 top[left]=targettop[left] = target

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;
}

参考

# 重识动态规划