LeetCode - 354. Russian Doll Envelopes及最长上升子序列问题总结

33 篇文章 0 订阅

LeetCode - 354. Russian Doll Envelopes及最长上升子序列问题总结

  • 最长上升子序列普通dp法
  • 最长上升子序列解的打印
  • 最长上升子序列NlogN法
  • LeetCode - 354. Russian Doll Envelopes

LeetCode300 测试最长上升子序列

最长上升子序列普通dp法

生成长度为N的数组dpdp[i]表示的是在以arr[i]这个数结尾的情况下,arr[0...i]中的最长递增子序列长度

  • 对第一个数arr[0]来说,dp[0] = 1,最长递增子序列就是自己。
  • 当计算到dp[i]的时候,最长递增子序列要以arr[i]结尾,所以我们在arr[0....i-1]中所有比arr[i]小的数可以作为最长递增子序列的倒数第二个数,这些数中,哪个的最长递增子序列更大,就选择哪个。即dp[i] = max(dp[j] + 1) ,0 <= j < i,arr[j] < arr[i]
 /**
 * dp[i]表示以arr[0]结尾的情况下,arr[0...i]中的最大递增子序列
 */
  class Solution {
    public int lengthOfLIS(int[] nums) {
        if(nums == null || nums.length == 0)
            return 0;
        int[] dp = new int[nums.length];
        int res = 1;
        for(int i = 0; i < nums.length; i++){
            dp[i] = 1;
            for(int j = 0; j < i; j++){
                if(nums[j] < nums[i])
                    dp[i] = Math.max(dp[i], dp[j]+1);
                res = Math.max(res,dp[i]);
            }
        }
        return res;
    }   
}

也可以和普通动态规划一样写成递归(记忆化的):

class Solution { 
    public int res;
    public int lengthOfLIS(int[] nums) {
        if(nums == null || nums.length == 0)
            return 0;
        int[] dp = new int[nums.length];
        Arrays.fill(dp, -1);
        res = 1;
        recur(nums, nums.length-1, dp);
        return res;
    }
    
    public int recur(int[] nums, int i, int[] dp){
        if(i == 0)
            return 1;
        if(dp[i] != -1)
            return dp[i];
        int max = 1;
        for(int k = 0; k < i; k++){            
            int kCur = recur(nums, k, dp); // remember to recursive
            if(nums[i] > nums[k])
                max = Math.max(kCur+1, max);
        }
        res = Math.max(res, max);
        return dp[i] = max;
    }
}

另一种递归的写法就是,直接在主程序中求每个位置结尾的最长递增子序列,然后记录一个最大值即可,这样递归函数就只有当nums[i] > nums[k]的时候才递归,而不是所有的时候都需要递归了。

class Solution {
   
    public int lengthOfLIS(int[] nums) {
        if(nums == null || nums.length == 0)
            return 0;
        int[] dp = new int[nums.length];
        Arrays.fill(dp, -1);
        recur(nums, nums.length-1, dp);
        int res = 1;
        for(int i = 0; i < nums.length; i++) // 求每个位置结尾的最长递增子序列,然后记录一个最大值即可
            res = Math.max(res, recur(nums, i, dp));
        return res;
    }
    
    public int recur(int[] nums, int i, int[] dp){
        if(i == 0)
            return 1;
        if(dp[i] != -1)
            return dp[i];
        int max = 1;
        for(int k = 0; k < i; k++){            
            if(nums[i] > nums[k])
                max = Math.max(recur(nums, k, dp) + 1, max);
        }
        return dp[i] = max;
    }
}

最长上升子序列解的打印

根据上面的方法可以求得dp数组,我们根据dp数组就可以得到最长上升子序列的解。

  • dp数组中的最大值dp[maxi]表示的是以arr[maxi]结尾的,而且是最长的上升子序列;
  • 我们从maxi往前面找,如果前面的某个dp[i]满足arr[i] < arr[maxi] 且dp[maxi] = dp[i] + 1,就说明这个是我们找最长递增子序列时候取的值,可以作为最长递增子序列的倒数第二个数。
  • 然后依次往前找,可以得到解。
    public static int[] getLis(int[] arr,int[] dp){
        int len = 0;
        int index = 0;
        for(int i = 0; i < dp.length; i++){
            if(dp[i] > len){
                len = dp[i];
                index = i;
            }
        }
        int[] lis = new int[len];
        lis[--len] = arr[index];
        for(int i = index - 1; i >= 0; i--){
            if(dp[i] == dp[index] - 1 && arr[i] < arr[index]){
                lis[--len] = arr[i];
                index = i;
            }
        }
        return lis;
    }

最长上升子序列NlogN法

这个方法是使用一个额外的数组ends[]dp[i]记录的还是以arr[i]结尾的最长递增子序列,ends[i]记录的是在所有长度为i的递增序列中,最小的结尾数是ends[i]。初始时整形变量right = 1ends[1...right]为有效区,ends[right+1...arr.length]为无效区,然后使用二分的方式在ends数组中查找。

  • dp[0] = 1ends[1] = arr[0]表示的是,在所有长度为1的递增序列中,最小结尾是arr[0],此时只有一个数;
  • right变量记录ends数组的有效范围,最后返回的就是right(end的有效范围);
  • 遍历到某个数arr[i]的时候,在ends[]数组中二分查找最左边的>arr[i]的数(二分查找看这篇博客)
    • 如果有某个位置l,即arr[l] > arr[i],说明l长度的最小结尾可以更新为arr[i],且dp[i] = right(目前的最长长度);
    • 否则,如果没有找到(此时l = right+1),则说明有效区长度要扩大1,且end[right+1] = arr[i],表示长度为right + 1的最小结尾暂时是arr[i],此时dp[i] = right + 1
  • 一直遍历整个数组,最后的最长长度就是有效区的长度(right);
/**
* dp[i]记录的还是以arr[i]结尾的最长递增子序列
* ends数组中 ends[i]表示的是在所有长度为i的最长递增子序列中最小结尾是什么
*/
class Solution { 
    public int lengthOfLIS(int[] nums){
        if(nums == null || nums.length == 0)
            return 0;
        int[] dp = new int[nums.length];
        int[] ends = new int[nums.length+1];
        dp[0] = 1;
        ends[1] = nums[0];
        int right = 1;  // [1~right]为有效区 ends数组是有序的(升序), right是右边界
        int l = 0,m = 0,r = 0;
        for(int i = 1; i < nums.length; i++){
            l = 1;
            r = right;
            while(l <= r){
                m = l + (r-l)/2;
                if(nums[i] > ends[m]){
                    l = m+1;
                }else {
                    r = m-1;
                }
            }
            if(l == right+1){ //没有找到 arr[i]是最长的, 说明以arr[i]以arr[i]结尾的最长递增子序列=ends区有效长度+1
                dp[i] = right+1;
                ends[right+1] = nums[i]; // 扩大ends数组
                right += 1;  //扩大有效区
            }else {  // 找到了arr[l] > arr[i], 更新end[l] = arr[i] ,表示l长度的最长子序列结尾可以更新为arr[i]
                dp[i] = right; // dp[i]还是没有加长
                ends[l] = nums[i];
            }
        }
        return right;
    } 
}

一个测试用例:
在这里插入图片描述


LeetCode - 354. Russian Doll Envelopes

题目链接
题目

在这里插入图片描述

解析

求解过程:先按a从小到大进行排序,当a相同时,按b从大到小排序。然后求解b的最长递增子序列。

为什么b要按从大到小排列呢?按照最长递增子序列的O(N*logN)方法,当前数arr[i]大于ends数组中所有的数(末尾的最大),我们会将arr[i]添加在ends数组中;否则在ends数组中二分查找第一个大于当前数的数且替换它。所以我们的做法会保证在a相等的情况下,b可以有一个最小值,这样可以摞相对多的数。以达更长的序列,同时也避免了a相同b不相同时摞在一起的情况

class Solution {
    
    private class Node{
        public int a;
        public int b;

        public Node(int a, int b) {
            this.a = a;
            this.b = b;
        }
    }

    public class NodeComparator implements Comparator<Node>{

        @Override
        public int compare(Node o1, Node o2) {
            if(o1.a == o2.a){
                return o2.b - o1.b;
            }else {
                return o1.a - o2.a;
            }
        }
    }

    
    
    public int maxEnvelopes(int[][] envelopes) {
        if(envelopes == null || envelopes.length == 0 || envelopes[0].length == 0)
            return 0;
        Node[] nodes = new Node[envelopes.length];
        for(int i = 0; i < envelopes.length; i++){
            nodes[i] = new Node(envelopes[i][0],envelopes[i][1]);
        }
        Arrays.sort(nodes,0,nodes.length,new NodeComparator());
        int[] ends = new int[envelopes.length+1];
        ends[1] = nodes[0].b;
        int right = 1;
        int l = 0, m = 0, r = 0;
        int res = 1;
        for(int i = 1; i < nodes.length; i++){
            l = 1;
            r = right;
            while(l <= r){
                m = l + (r-l)/2;
                if(nodes[i].b > ends[m]){
                    l = m + 1;
                }else {
                    r = m - 1;
                }
            }
            if(l == right+1){
                ends[right+1] = nodes[i].b;
                right += 1;
            }else {
                ends[l] = nodes[i].b;
            }
        }
        return right;
    }
}
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值