编程之美2.16-最长递增子序列(Longest Subsequence)

本文介绍了求解最长递增子序列的经典问题及其三种高效算法,包括动态规划、记录序列最大值最小值以及利用二叉搜索树进行优化的方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最长子序列问题(Longest Subsequence)

猴子摘桃问题

首先用一道阿里笔试题引出最长子序列问题:

小猴子下山,沿着下山的路有一排桃树,每棵树都结了一些桃子。小猴子想摘桃子,但是有一些条件需要遵守,小猴子只能沿着下山的方向走,不能回头,每颗树最多摘一个,而且一旦摘了一棵树的桃子,就不能再摘比这棵树结的桃子少的树上的桃子。那么小猴子最多能摘到几颗桃子呢?
举例说明,比如有5棵树,分别结了10,4,5,12,8颗桃子,那么小猴子最多能摘3颗桃子,来自于结了4,5,8颗桃子的桃树。

这里我们发现该问题的本质是求解给定数列的最长递增子序列,比如10,4,5,12,8中的最长递增子序列是4,5,8,也可以是4,5,12,长度是3.

解法一


记录以当前元素为最大元素的最长序列长度
首先最简单的思路:

  • 开辟一个lis数组,记录以arr[i]作为最大元素子序列的长度。

  • 遍历到下一个元素arr[i+1],遍历lis数组前i个元素,

  • 如果比lis[k]大,说明该元素可以作为 以lis[k]为最大元素的这个递 增子序列的最后一个元素。
  • 然后lis[k]+1,更新当前lis[i+1]的值,在遍历前i个元素后存储最大的值,举例
    arr:12,4,5,8,13,14,9
    lis:1,1,2,3,2,3,4
    新增元素20,对应lis[7]
    经历1+1,2+1,3+1,4+1最后等于最大的4+1=5
    最后遍历所有lis,最大的元素即为最长递增子序列的长度
public class LIS_A {
    public int lis(int arr[]){
        assert arr!=null:"数组为空";//临界值测试 数组长度为0,1分别返回0,1,正常
        int arrL=arr.length;
        int[] lis=new int[arrL];//lis[i]表示以arr[i]作为最大元素 子序列的长度
        for(int i=0;i<arrL;i++){
            //即使没有其他更小元素,当前元素本身就是长度为1的递增子序列,所以初始长度为1
            lis[i]=1;
            for(int j=0;j<i;j++){
                //如果arr[i]>arr[j]并且lis[j]+1>lis[i],因为之前可能有很多子序列,所以保证lis[i]是最长的
                if(arr[i]>arr[j]&&lis[j]+1>lis[i])
                    lis[i]=lis[j]+1;
            }
        }
        return max(lis);
    }
    /*返回数组最大值O(n)*/
    public int max(int arr[]){
        int max=0;
        for (int i = 0; i < arr.length; i++) {
            if(arr[i]>max)
                max=arr[i];
        }
        return max;
    }
}

该解法每次都要遍历前面所有lis[k] 所以输入n个元素 时间复杂度是O(n^2+n)

解法二


记录某一长度序列的最大元素最小值
解法一 每到一个元素,都要遍历之前的所有lis元素,更好的思路是:
记录下 不同长度的子序列的最小值:MaxV[k] (长度为k的子序列的最小值)

然后对比当前元素会有两种情况:

  • 如果当前元素MaxV[k] < arr[i]< MaxV[k+1]说明arr[i]可以加在MaxV[k]后面成为新的k+1长的序列;
  • 又因为小于MaxV[k+1],所以当前arr[i]应该取代MaxV[k+1]成为k+1长序列新的最小值:MaxV[j+1]=arr[i]
  • 如果当前当前元素 arr[i] > MaxV[k] (MaxV[k]是MaxV数组最后的元素,k是当前最长的长度)
  • 所以加入arr[i]后会构成新的最长k+1; 因而MaxV[k+1]=arr[i]

public class LIS_B {
    public int lis(int arr[]){
        assert arr.length>0:"输入错误";
        int arrL=arr.length;
        int[] lis=new int[arrL];
        /*MaxV是个比较难理解的地方
        MaxV[j] 记录长度为j的许多序列中 最大值的最小值,所以新值只要大于这个值就可以插在后面构成j+1长的新子数组
        最长可能为arrL所以MaxV[arrL]也得有
        MaxV[0]是数组最小值减1,存在的意义是为了更新MaxV[1]
        比如[5,6]插入1,长度为1的子序列最大元素最小值MaxV[1]此刻是5,插入1那么这个最小值就应该是1
        而插入机制是MaxV[j]<arr[i]&&arr[i]<MaxV[j+1],意味着arr[i]插入构成j+1的新数列,因而要覆盖Max[j+1]成为新的最小值
        所以此时MaxV[0]<1<MaxV[1],1会写入MaxV[1]成为新的长度为1的子序列最大元素最小值*/
        int[] MaxV=new int[arrL+1];
        MaxV[1]=arr[0];//其实也可以不给MaxV[1]赋值(maxLength=0即可),但是比较大小时会很麻烦
        MaxV[0]=min(arr)-1;//临界值,元素至少能写入MaxV[1]

        int maxLength=1;
        for(int i=0;i<arrL;i++){
            int j;//子序列长度
            for(j=maxLength;j>=0;j--) { /*从maxLength开始找,一直到0,插入对应的位置*/
                if (arr[i] >MaxV[j]) { //非递增,或者递减可以在这里修改
                    lis[i] = j + 1;
                    break;
                }
            }
            //两种情况,大于当前最长,所以构成新的最长,maxLength+1,并把当前值赋给MaxV[lis[i]]等同maxV[maxLength]
            if(lis[i]>maxLength){
                maxLength=lis[i];
                MaxV[lis[i]]=arr[i];
            }
            else if(MaxV[j]<arr[i]&&arr[i]<MaxV[j+1]){ //非递增,或者递减可以在这里修改
                MaxV[j+1]=arr[i];
            }
        }
        return maxLength;


    }
    private int min(int arr[]){
        int min=arr[0];
        for (int i = 1; i < arr.length; i++) {
            if(arr[i]<min)
                min=arr[i];
        }
        return min;
    }
}

解法三-二叉搜索树 书上没有


之前解法二虽然优化后更快 但实际上还是O(n^2)的算法,

  • 定位- arr[i]到对应MaxV的位置,可以使用二叉搜索树
  • 总体复杂度降为O(nlogn),自己写了一版供大家参考。
/记录最小值,二分搜索树搜索(书上没有) 时间复杂度从O(N^2)降到O(N*logN)
public class LIS_C {
    public int lis(int arr[]){
        assert arr.length>0:"输入错误";
        int arrL=arr.length;
        int[] lis=new int[arrL];

        int[] MaxV=new int[arrL+1];
        MaxV[1]=arr[0];//其实也可以不给MaxV[1]赋值(maxLength=0即可),但是比较大小时会很麻烦
        MaxV[0]=min(arr)-1;//临界值,元素至少能写入MaxV[1]

        int maxLength=1;
        for(int i=0;i<arrL;i++){
            int j;//对应的次长子序列长度
            //两种情况,大于当前最长,所以构成新的最长,maxLength+1,并把当前值赋给MaxV[lis[i]]等同maxV[maxLength]
            if(arr[i]>MaxV[maxLength]) {
                j = maxLength;
                maxLength++;
                MaxV[maxLength] = arr[i];
            }
            else{
                j = binarySearch(arr[i], maxLength, MaxV);
                MaxV[j + 1] = arr[i];
            }
            lis[i]=j+1;
        }
        return maxLength;


    }
    //二叉搜索树搜索  推荐用双索引 而不是用递归  这样效率更高 MaxV[searchS]<input<=MaxV[searchE]  返回searchS
    private int binarySearch(int input,int maxLength,int MaxV[]){
        int searchS=0;   //游标起点
        int searchE=maxLength; //游标终点
        while(searchS+1<searchE){
            int searchM=searchS+(searchE-searchS)/2;
            if(input>MaxV[searchM])
                searchS=searchM;
            else
                searchE=searchM;
        }
        return searchS;

    }
    private int min(int arr[]){
        int min=arr[0];
        for (int i = 1; i < arr.length; i++) {
            if(arr[i]<min)
                min=arr[i];
        }
        return min;
    }
}

测试函数

public class Main {
    public static void main(String[] args) {
        int arr[]={12,3,4,5,13,6,7,14};
        System.out.println(Arrays.toString(arr));
        System.out.println("最长子序列长度为"+new LIS_A().lis(arr));
        }

运行结果: [12, 3, 4, 5, 13, 6, 7, 14]
最长子序列长度为6

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值