Leetcode面试题 17.08. 马戏团人塔——最长递增子序列问题

引入

今天做到面经的17.08.马戏团人塔🔗面试题 08.13. 堆箱子问题,两个题目很相似,这里会一一介绍。

马戏团人塔问题分析

首先是马戏团人塔问题,题目要求在2个维度上(即身高 + 体重)同时保持严格递增。
那么我们可以先将其中一个维度排好序,以保证在一个维度上保持递增(此时并非严格递增);
之后就可以专注于处理另一个维度。

具体而言:
先根据身高升序排序,若身高一样则根据体重降序排序。身高排序好之后,剩余待处理的就是体重。
处理体重的问题就是处理最长递增子序列的问题。

为什么要按照身高升序,而按照体重降序呢?为什么不能两个都是升序呢?

我们查看下面的序列(已经排序好了):
身高:[1, 2, 2, 3, 4]
体重:[1, 3, 4, 2, 7]
按身高和体重同时升序排列,那么,体重的最长递增子序列是[1, 3, 4, 7],但是题目要求是完全递增,不能产生等于的情况。
那么如果按照身高升序,按照体重降序又是什么样子的呢?
身高:[1, 2, 2, 3, 4]
体重:[1, 4, 3, 2, 7]
此时,体重的最长递增子序列是[1, 4, 7]或者[1,3,7],这个时候就能保证身高递增且体重也是递增的了。

现在就是来解决最长递增子序列问题。

最长递增子序列问题(LIS)解法

具体题目参考:300. 最长上升子序列🔗

方法一:动态规划

状态dp[i]表示前i个元素的最长递增子序列长度是dp[i]。

有了状态,那么状态转移方程也很好找到了:

  1. 前面有元素j比元素i小,那么:
    dp[i]=max(dp[j]),j>=0&&j<i
  2. 前面没有元素j比元素i小,那么:
    dp[i]=1

所以初始状态也出来了:dp[0]=1

那么,题解为:

class Solution {
    public int lengthOfLIS(int[] nums) {
        if(nums.length==0) return 0;
        int[] dp=new int[nums.length+1];
        dp[0]=1;
        int max=1;
        for(int i=0;i<nums.length;i++){
            for(int j=i-1;j>=0;j--){//往前搜索
                if(nums[i]>nums[j]){
                    dp[i+1]=Math.max(dp[j+1]+1,dp[i+1]);
                    max=Math.max(dp[i+1],max);
                }
            }
            if(dp[i+1]==0) dp[i+1]=1;//前面的j都比i大
        }
        return max;
    }
}

由于每次找dp[i]的时候都要向前一个一个搜索,这种动态规划的算法时间复杂度是O(N^2)。

方法二:贪心 + 二分查找

考虑一个简单的贪心,如果我们要使上升子序列尽可能的长,则我们需要让序列上升得尽可能慢,因此我们希望每次在上升子序列最后加上的那个数尽可能的小。

基于上面的贪心思路,我们维护一个递增数组 d [ i ] d[i] d[i],表示长度为 i i i 的最长上升子序列的末尾元素的最小值,用 len \textit{len} len 记录目前最长上升子序列的长度,起始时 l e n len len 1 1 1 d [ 1 ] = nums [ 0 ] d[1] = \textit{nums}[0] d[1]=nums[0]

同时,我们可以从上面的设定中知道,数组 d [ i ] d[i] d[i]是关于 i i i单调递增的,因为如果出现了 d [ i ] < d [ j ] d[i]<d[j] d[i]<d[j] i > j i>j i>j的情况,那么必然可以出现一个 k k k,使得 d [ k ] < d [ j ] d[k]<d[j] d[k]<d[j] k = j k=j k=j的情况。
具体举例来说,就是 [ a , b , 13 ] [a,b,13] [a,b,13] [ a , 15 ] [a,15] [a,15]的情况是不成立的,因为能够找到一个 [ a , b , 13 ] [a,b,13] [a,b,13] [ a , b ] [a,b] [a,b]的存在,其中 a < b < 13 a<b<13 a<b<13

根据 d [ i ] d[i] d[i]数组的单调性,就可以根据二分法来查找符合条件的那个 d [ i ] d[i] d[i]

用理性的逻辑来分析这个问题,这个问题的最关键因素是为什么能用贪心的方式去替换掉递增数组 d [ i ] d[i] d[i]的值,假如目前维护的递增数组 d [ i ] d[i] d[i]包含[1,4,8]三个元素,如果新增的元素 k k k比8更小,那么我们可以用贪心的方式让 d [ i ] d[i] d[i]的值更收敛,比如如果插入了6,那么能使得新数组[1,4,6]更收敛,但是它的长度不变;而如果插入的元素 k k k比8更大,那么必然存在一个长度为4的递增序列,其真实的数组数据不一定是[1,4,8, k k k],有可能真实数据是[1,5,8, k k k],虽然5被4替换了,但是这个5实际上是存在过的,贪心只是为了让后面的序列更加收敛,更容易的去找到合适的 k k k,所以这样的贪心是正确的。

那么,代码如下:

public class Solution {
    public int lengthOfLIS(int[] nums) {
        if (nums.length == 0) return 0;
        int[] d = new int[nums.length + 1];
        d[1] = nums[0];
        int currLen = 1;
        for (int i = 1; i < nums.length; i++) {
            if (nums[i] > d[currLen]) {
                d[++currLen] = nums[i];
            } else {
                //二分法
                int left = 1,right = currLen;
                while (left <= right) {
                    int mid = (left + right) >>> 1;//无符号右移一位
                    if (nums[i] > d[mid]) {
                        left = mid + 1;
                    } else right = mid - 1;
                }
                d[left]=nums[i];
            }
        }
        return currLen;
    }
}

利用 d [ i ] d[i] d[i]的单调性,我们可以把原本动态规划的O(N^2)的复杂度降低为O(NlogN)。

更简单的,我们可以利用Arrays提供的方法类来完成二分查找。
对于Arrays.binarySearch(int[] a,int num)方法,如果返回值是大于等于0的数,那么说明在二分查找中找到了这个数。如果返回值是小于0的数,那么将其反转后减去1,就是它应该在二分法里面插入的位置。

比如对于序列[1,3,4,5,7]:
使用Arrays.binarySearch(arr,3)会返回1。
使用Arrays.binarySearch(arr,2)会返回-2,则-(-2)-1=1就是应该插入的位置。

由于这里并不是利用全部的数组 d d d,所以可以简单修改为Arrays.binarySearch(d,0,currLen,nums[i])

public class Solution {
    public int lengthOfLIS(int[] nums) {
        if (nums.length == 0) return 0;
        int[] d = new int[nums.length + 1];
        d[1] = nums[0];
        int currLen = 1;
        for (int i = 1; i < nums.length; i++) {
            if (nums[i] > d[currLen]) {
                d[++currLen] = nums[i];
            } else {
                //二分法
                int index=Arrays.binarySearch(d,0,currLen,nums[i]);
                if (index<0) index=-index-1;
                d[index]=nums[i];
            }
        }
        return currLen;
    }
}

马戏团人塔问题解法

马戏团问题,我们这里采用贪心+二分查找的方式来解决(采用动态规划会导致超时)。

public class Solution {
    public int bestSeqAtIndex(int[] height, int[] weight) {
        int M=height.length;
        if(M==0) return M;
        int[][] person=new int[M][2];
        for (int i=0;i<M;i++){
            person[i]=new int[]{height[i],weight[i]};
        }
        Arrays.sort(person,(o1, o2) -> {
            if (o1[1]==o2[1]){
                return o2[0]-o1[0];//如果体重一样,就按照身高降序
            }
            return o1[1]-o2[1];//按体重升序
        });
        int[] d=new int[M+1];
        d[1]=person[0][0];
        int currLen=1;
        for (int i=1;i<M;i++){
            if (person[i][0]>d[currLen]){
                d[++currLen]=person[i][0];
            }else{
                int index=Arrays.binarySearch(d,0,currLen,person[i][0]);
                if (index<0) index=-index-1;
                d[index]=person[i][0];
            }
        }
        return currLen;
    }
}

这里通过排序的方法, 将二维的递增问题转换成了一维的递增问题。

堆箱子问题解法

这里的面试题 08.13. 堆箱子,新增了一个新的纬度,那么应该怎么解呢?是否可以化简为二维或者一维呢?

这里,我们仍然可以使用最长递增子序列的方法,不过这里已经不能使用(贪心+二分查找)的方法了,因为要多比较一个纬度,使用动态规划最为划算。

public class Solution {
    public int pileBox(int[][] box) {
        Arrays.sort(box,(o1, o2) -> {
            if (o1[0]==o2[0]){
                if (o1[1]==o2[1]){
                    return o1[2]-o2[2];
                }else
                    return o1[1]-o2[1];
            }else
                return o1[0]-o2[0];
        });
        // Arrays.stream(box).forEach(o-> System.out.println(o[0]+" "+o[1]+" "+o[2]));
        int[] dp=new int[box.length+1];
        dp[0]=box[0][2];
        int max=dp[0];
        for (int i=1;i<box.length;i++){
            dp[i]=box[i][2];
            for (int j=0;j<i;j++){
                if (box[i][2]>box[j][2]&&box[i][1]>box[j][1]&&box[i][0]>box[j][0]){
                    //为什么0,1,2都要比较,要去除等于的情况
                    dp[i]=Math.max(dp[i],dp[j]+box[i][2]);
                }
            }
            max=Math.max(max,dp[i]);
        }
        return max;
    }
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值