牛客网编程高频题32——NC91最长递增子序列

目录

最长递增子序列

描述

示例1

示例2

方法一:动态规划(超时)

方法二:动态规划+二分法


最长递增子序列

描述

给定数组arr,设长度为n,输出arr的最长递增子序列。(如果有多个答案,请输出其中字典序最小的)

示例1

输入

[2,1,5,3,6,4,8,9,7]

返回值

扫描二维码关注公众号,回复: 13161589 查看本文章
[1,3,4,8,9]

示例2

输入

[1,2,8,6,4]

返回值

[1,2,4]

 说明

其最长递增子序列有3个:(1,2,8),(1,2,6),(1,2,4)其中第三个按数值进行比较的字典序最小,故答案为(1,2,4)

分析:对于给出的数组,要从其中找到一个子序列,满足序列中的数字是单调上升的,在满足的子序列中找到长度最长的并进行输出。子序列是指一个数组删除若干元素,但不改变其他元素相对位置形成的序列

方法一:动态规划(超时)

我们使用dp[i]表示前i个数中,以第i个数字结尾的最长上升子序列的长度,且必须要选arr[i]元素,那么最大上升序列必须要满足arr[i]>arr[j],并且i>j,所以我们可以遍历前i个元素,找到最大的dp[j],并且满足arr[i]>arr[j],那么就可以在dp[j]的基础上加1, 

由此我们可以得到状态方程:

dp[i]=max(dp[j])+1, \; 0\le j <i \;and\;arr[j]<arr[i],

我们以数组【2,4,5,3,7,1】为例用动态规划的方法求解最长递增子序列的长度,

 知道了最长递增子序列的长度和结尾的元素位置,我们就可以从结尾元素开始倒叙遍历,将小于该元素的递减子序列(因为是倒叙)求出来了,即【2,4,5,7】

public static int[] DynamicPlanning(int []arr){
    int n = arr.length;
    int[] dp =new int[n];
    int maxn=0;
    for (int i = 0; i < n; i++) {
        int len=0;
        for (int j = 0; j < i; j++) {
            if (arr[i]>arr[j]){//对每个比arr[i]小的元素,比较长度
                len=Math.max(len,dp[j]);
            }
        }
        dp[i]=len+1;
        maxn=Math.max(maxn,dp[i]);
    }
    
    int res[]=new int[maxn];
    for (int i = n-1,j=maxn; j>0 ; i--) {
        if (dp[i]==j){
            res[--j]=arr[i];
        }
    }
    return res;
}

该方法状态数量为n,并且每个状态需要O(n) 的时间,故总时间复杂度为O(n^2)

方法二:动态规划+二分法

上述方法每个状态需要O(n) 的时间,如果使用贪心的方法就可以优化这部分时间,

对于序列而言,如果想让上升子序列尽可能地长,那么就要在末尾的时候添加元素尽可能的小,比如对于一个序列【1,2,5,3】而言,如果是构建长度为3的子序列,那么就会选择【1,2,3】而不是【1,2,5】。

我们使用数字d[i]表示长度为i的最长上升子序列的末尾数字最小值,所以d[i]是单调递增的,同时使用len记录当前最长上升子序列的长度,

算法的思路为:遍历数组arr中的每个元素,并且对d数组和len的值进行更新,

  • arr[i]>d[len],更新len=len+1
  • 如果arr[i]<d[len],那么就意味着arr[i]结尾的递增子序列长度不是最长的,我们假设它的长度为n,那么我们就可以更新长度为n的递增子序列的末尾数字最小值d[n],这样我们就可以不断更新不同长度的递增子序列的末尾数字最小值数组d[],那么我们如何确定arr[i]对应的长度n,这里我们使用二分法,找到满足d[n-1]<arr[i]<d[n]的下标n进行更新
  • 因为要返回子序列,所以使用辅助数组p[i]表示当前位置的最长递增子序列长度,最后倒叙遍历p[i]找到该最长递增子序列
public static int[] LIS (int[] arr) {
    // write code here
    int n = arr.length;
    if (n < 2) return arr;//特判空情况,方便后面处理

    int len=1;//最长递增子序列长度
    int d[]=new int[n+1];//长度为i的最长上升子序列的末尾数字最小值
    int p[]=new int[n];//记录以当前元素结尾时最长递增序列长度
    d[len]=arr[0];
    p[0]=1;

    for (int i = 1; i < n; i++) {
        if (arr[i]>d[len]){
            d[++len]=arr[i];//更新长度为len+1时的末尾数字最小值
            p[i]=len;//记录当前位置元素结尾时最长长度
        }else{//否则开始二分,查找以arr[i]结尾时的最长递增序列长度
            int left=1,right=len,pos=0;
            while(left<=right){
                int mid=(left+right)/2;
                if (d[mid]<arr[i]){//如果该长度的末尾数字最小值比arr[i]小,则将mid的值调大
                    pos=mid;
                    left=mid+1;
                }else{//如果该长度的末尾数字最小值比arr[i]大,则将mid的值调小
                    right=mid-1;
                }
            }
            d[pos+1]=arr[i];//更新长度为pos+1时末尾最小数字
            p[i]=pos+1;//记录以当前元素结尾时最长长度
        }
    }

    int res[]=new int[len];
    for (int i = n-1; i >= 0 ; i--) {
        if (p[i]==len){
            res[--len]=arr[i];
        }
    }
    return res;
}

猜你喜欢

转载自blog.csdn.net/weixin_39478524/article/details/117820381
今日推荐