题目

类型:动态规划

image.png

解题思路

这道题目其实是最长递增子序列的一个变种,因为每次合法的嵌套是大的套小的,相当于在二维平面中找一个最长递增的子序列,其长度就是最多能嵌套的信封个数
前面说的标准 LIS 算法只能在一维数组中寻找最长子序列,而我们的信封是由 (w, h) 这样的二维数对形式表示的,如何把 LIS 算法运用过来呢?
image.png
读者也许会想,通过 w × h 计算面积,然后对面积进行标准的 LIS 算法。但是稍加思考就会发现这样不行,比如 1 × 10 大于 3 × 3,但是显然这样的两个信封是无法互相嵌套的。
这道题的解法比较巧妙:
先对宽度 w 进行升序排序,如果遇到 w 相同的情况,则按照高度 h 降序排序;之后把所有的 h 作为一个数组,在这个数组上计算 LIS 的长度就是答案
画个图理解一下,先对这些数对进行排序:
image.png
然后在 h 上寻找最长递增子序列,这个子序列就是最优的嵌套方案:
image.png
为什么呢?稍微思考一下就明白了:
首先,对宽度 w 从小到大排序,确保了 w 这个维度可以互相嵌套,所以我们只需要专注高度 h 这个维度能够互相嵌套即可。
其次,两个 w 相同的信封不能相互包含,所以对于宽度 w 相同的信封,对高度 h 进行降序排序,保证 LIS 中不存在多个 w 相同的信封(因为题目说了长宽相同也无法嵌套)。

代码

  1. public class RussianDollEnvelopes {
  2. /**
  3. * envelopes = [[w, h], [w, h]...]
  4. */
  5. public int maxEnvelopes(int[][] envelopes) {
  6. int n = envelopes.length;
  7. // 按宽度升序排列,如果宽度一样,则按高度降序排列
  8. Arrays.sort(envelopes, new Comparator<int[]>() {
  9. @Override
  10. public int compare(int[] a, int[] b) {
  11. return a[0] == b[0] ?
  12. b[1] - a[1] : a[0] - b[0];
  13. }
  14. });
  15. // 对高度数组寻找 LIS
  16. int[] height = new int[n];
  17. for (int i = 0; i < n; i++) {
  18. height[i] = envelopes[i][1];
  19. }
  20. return lengthOfLIS(height);
  21. }
  22. int lengthOfLIS(int[] nums) {
  23. int len = nums.length;
  24. if (len <= 1) {
  25. return len;
  26. }
  27. // tail 数组的定义:长度为 i + 1 的上升子序列的末尾最小是几
  28. int[] tail = new int[len];
  29. // 遍历第 1 个数,直接放在有序数组 tail 的开头
  30. tail[0] = nums[0];
  31. // end 表示有序数组 tail 的最后一个已经赋值元素的索引
  32. int end = 0;
  33. for (int i = 1; i < len; i++) {
  34. int left = 0;
  35. // 这里,因为当前遍历的数,有可能比有序数组 tail 数组实际有效的末尾的那个元素还大
  36. // 【逻辑 1】因此 end + 1 应该落在候选区间里
  37. int right = end + 1;
  38. while (left < right) {
  39. // 选左中位数不是偶然,而是有原因的,原因请见 LeetCode 第 35 题题解
  40. // int mid = left + (right - left) / 2;
  41. int mid = (left + right) >>> 1;
  42. if (tail[mid] < nums[i]) {
  43. // 中位数肯定不是要找的数,把它写在分支的前面
  44. left = mid + 1;
  45. } else {
  46. right = mid;
  47. }
  48. }
  49. // 因为 【逻辑 1】,因此一定能找到第 1 个大于等于 nums[i] 的元素
  50. // 因此,无需再单独判断,直接更新即可
  51. tail[left] = nums[i];
  52. // 但是 end 的值,需要更新,当前仅当更新位置在当前 end 的下一位
  53. if (left == end + 1) {
  54. end++;
  55. }
  56. }
  57. // 此时 end 是有序数组 tail 最后一个元素的索引
  58. // 题目要求返回的是长度,因此 +1 后返回
  59. end++;
  60. return end;
  61. }
  62. }