困难题 354 俄罗斯套娃信封问题

290 阅读6分钟

1 问题描述

输入一个二维数组,每个元素代表一个信封,信封内每个元素代表了一个信封,一个信封是一个长度为2的数组,存储了宽和长

  • 长和宽都比另一个信封小的情况下,能够装入另一个信封
  • 求解最多能够多少个信封套在一起
  • 信封不能旋转

2 选择排序后遍历

解题思路

  • 从小到大排序,对于信封按照宽度进行排序,宽度一致时按照高度进行排序,排序方式采用选择排序
  • 通过遍历的方式求出套娃的最大值
    • 第一个信封套娃数量为1,记录上一个信封为当前信封,整套信封套娃数量最大值暂时计为1
    • 第二个信封开始
      • 比较当前信封的宽度和上一个信封的宽度
        • 如果宽度相等,跳过当前信封,在宽度一样的情况下信封高度越小越好,而上一个信封高度必定更小
        • 如果宽度比上一个信封大,比较当前信封的高度和上一个信封的高度
          • 如果高度大于上个信封,那么可以将上个信封装入当前信封,记录上一个信封为当前信封,整套信封最大套娃数量加一
          • 如果高度小于上个信封,跳过当前信封
  • 算法复杂度:O(n^2)
    • 选择排序 O(n^2)
    • 遍历求套娃最大值 O(n)

以上解题思路确实不正确,感觉像是只考虑了宽,是对宽度进行贪心的情况,换一个角度,如果先对于高度进行排序,其中宽度作为第二次序,有可能得到不一样的答案。

如果保持上边的思路不变,希望正确求解的话,在求出了优先对宽排序的情况下的套娃最大值后,再次对高度进行排序,宽度作为第二次序,重复一遍上边的步骤,得到一个优先对高度进行排序的套娃最大值,最后两者进行比较,得到一个套娃最大值

感觉还是不对,从两个片面的解当中选出一个大的,不能代表这个解就是正确的,综合考量宽度和高度的话有可能得到更大的套娃值

下面是我一半正确的代码,通过32 / 84 个通过测试用例,感觉思路走错方向了就没有再改

class Solution {
   public int maxEnvelopes(int[][] envelopes) {
       int n = envelopes.length;
       if(n == 1){
           return 1;
       }

       int min = 0;
       for(int i = 0; i < n - 1; i++){
           min = i;
           for(int j = i + 1; j < n; j++){
               if(envelopes[min][0] > envelopes[j][0]){
                   min = j;
               }else if(envelopes[min][0] == envelopes[j][0] ){
                   min = (envelopes[min][1] < envelopes[j][1])?min:j;
               }
           }

           swap(envelopes,min,i);
       }

       int most = 1;
       int last = 0;
       for(int i = 1; i < n; i++){

           if(envelopes[i][0]  > envelopes[last][0] && envelopes[i][1] > envelopes[last][1]){
               most ++;
               last = i;
           }
       }

       return most;
   }

   public void swap(int[][] envelopes, int i, int j){
       int tmpW = envelopes[i][0];
       int tmpH = envelopes[i][1];
       
       envelopes[i][0] = envelopes[j][0];
       envelopes[i][1] = envelopes[j][1];

       envelopes[j][0] = tmpW;
       envelopes[j][1] = tmpH;
   }
}

3 排序后遍历+二分查找

解题思路

  • 排序,希望宽度是从小到大的顺序,宽度相等时,高度是从大到小的顺序
  • 遍历,使用一个链表来记录套娃的信封的序号
    • 如果当前的信封高度大于套娃信封的最后一个信封高度,这个时候直接将这个信封作为最大信封,放到链表末尾
    • 如果当前的信封高度小于等于套娃信封的最后一个信封高度,那我们在链表中找到一个位置j0,使得j0的高<当前的高,当前的高<=j0 + 1的高,将j0 + 1的信封改为当前

Q:遍历时第二种情况的疑问:这样修改链表某个结点的高度,宽度的递增顺序不就没有用了吗
修改最后一个结点时,要么宽度和其相等,要么宽度大于它

修改一个结点后面的结点,说明修改后后面的结点宽度一定大于前面的结点(同一宽度,高度从大到小排列)

我们希望的是在宽度顺序增大的基础上,高度增加的越慢越好

  • 修改链表中的结点,并不会改变链表的长度,也就是说链表依然记录了到当前位置的套娃最大个数
  • 修改分为两种,修改链表最后一个结点,修改链表其他位置结点
    • 修改最后一个结点,表明其高度大于倒数第二个结点,但是小于最后一个结点,修改这个结点有利于1后面的结点直接添加到链表后面
    • 修改的不是最后一个结点,表明在被修改的这个结点存在这么一种信封的放置方案,此时链表中宽度严格单调递增的规律可能已经被打破了,但是
      • 不影响后续信封高度链接到最后一个结点
      • 当前链表依旧记录了到当前位置的最大套娃数
      • 后续结点参照这个修改的高度修改进来有影响吗,没有
        • 如果这个结点的高度作为上界被后面的结点参考并采用,宽度更大的结点可能会修改到这个结点前,或者覆盖这个结点,但这都不影响,前面被修改的这部分只是覆盖了原来的放置
        • 如果这个结点的高度作为下界被后面的结点参考并采用,前置条件是后面的结点高度大于当前结点高度,由于同一宽度下信封按高度从大到小排列,他们不可能是同一宽度的结点,只能是不同宽度,而后者的宽度必然大于前者的宽度,说明修改之后,其宽度是存在递增的放置方案的。

Q:为什么需要高度从大到小排序

  • 因为在进行遍历的时候,我们的关注度都放在了高度上,我们并不知道宽度是大于前面还是等于前面,如果高度顺序从小到大,按照我们的逻辑,如果这个宽度的第一个信封高度就大于链表末尾结点,这个宽度的所有信封都会链接到链表上,会造成最大嵌套数溢出的错误。

  • 假设存在n种宽度的信封,而每个宽度的信封都有若干种高度,那么最多的嵌套数就是n,嵌套数不能超过n。链接同一个宽度的多个信封必然是错误的

  • 添加最后一个结点时,如果高度是逆序,不可能存在同一宽度连续添加

  • 时间复杂度 ;O(nlogn)

    • 排序 O(nlogn), 这里采用了其他的排序方法
    • 遍历 + 二分查找: O(nlogn)
class Solution {
    public int maxEnvelopes(int[][] envelopes) {
        int n = envelopes.length;
        Arrays.sort(envelopes, new Comparator<int[]>() {
            public int compare(int[] e1, int[] e2) {
                if (e1[0] != e2[0]) {
                    return e1[0] - e2[0];
                } else {
                    return e2[1] - e1[1];
                }
            }
        });

        List<Integer> f = new ArrayList<Integer>();
        f.add(envelopes[0][1]);
        for (int i = 1; i < n; ++i) {
            int num = envelopes[i][1];
            if (num > f.get(f.size() - 1)) {
                f.add(num);
            } else {
                int index = binarySearch(f, num);
                f.set(index, num);
            }
        }
        return f.size();
    }

    public int binarySearch(List<Integer> f, int target) {
        int low = 0, high = f.size() - 1;
        while (low < high) {
            int mid = (high - low) / 2 + low;
            if (f.get(mid) < target) {
                low = mid + 1;
            } else {
                high = mid;
            }
        }
        return low;
    }
}