最长递增子序列

问题描述:

  • 给定一个序列,求解其中长度最长的递增子序列,最长递增子序列表示必须递增但是可以位置不连续的序列。
  • 例如:{4 2 3 1 5 }的最长递增子序列为 2 3 5,长度为 3 。

算法概述:

  • 还是老样子,从最后一步来看是否可以用动态规划的思想去解决问题
  • 设F(n)为前n个数的最长子序列长度
  • 设我们已经求出了F(4)如何去求出F(5)呢?其实很简单,我们只需要比较num【5】和F(4)中最长子序列的尾数相比:
  • 如果num【5】大于F(4)中最长子序列的尾数,那么F(5) = F(4)+ 1
  • 如果num【5】小于F(4)中最长子序列的尾数,那么F(5)= F(4)
  • 问题的出现: 我们都知道我们会使用一个dp矩阵去存储中间的计算结果,但是根据上面的情况,我们每次判断时需要用到子问题的最长子序列尾数,返回时则需要用到子问题的最长序列长度。而我们的dp矩阵只能存储一种值,这就是问题的所在。也是我在思考这道题的时候无法进行解决的。
  • 问题解决: 两种设计算法
  • dp【i】表示以num【i】结尾的最长子序列长度
  • dp[i] 表示长度为 i 的最长递增子序列(LIS)末尾的数。
  • 这两种存储方法都可以去同时知道最长子序列长度和其尾数

算法思路一:

  • dp【i】表示num【i】结尾的最长子序列长度,例如:{4 6 2 7 1 9 }
  • 首先遍历 4 ,以 4 结尾的最长子序列长度显然为1,则dp【0】 = 1
  • 之后遍历 6 ,6 去和 4 比较,因为 6 > 4,所以dp【1】 = dp【0】+ 1 = 2
    也就是以 6 结尾的最长子序列的长度为2
  • 之后遍历 2 ,由于 2 < 4, 2 < 6,所以以 2 结尾的最长子序列为 1, dp【2】 = 1;
    也就是以 2 结尾的最长子序列的长度为1
  • 之后遍历 7
  • 因为7 > 4,所以dp【3】可能等于 L1 = dp【0】+ 1 = 2
  • 因为7 > 6,所以dp【3】可能等于 L2 = dp【1】+ 1 = 3
  • 因为7 > 2,所以dp【3】可能等于 L3 = dp【2】+ 1 = 2
  • 最终dp【3】 = Max(L1, L2, L3) = L2 = 3
  • 也就是以 7 结尾的最长子序列的长度为3
  • 之后遍历1,dp【4】 = 1
  • 最后遍历9. 显然dp【5】 = dp【3】+ 1 = 4
  • 最终要取dp矩阵中的最大值
  • 这种算法的时间复杂度为O(n2),所以下面给出一种优化算法
public static int maxQueue(){

        int maxLength = 1;
        //dp[i] 表示以 i 结尾的最长递增子序列长度
        int[] dp = new int[nums.length];
        dp[0] = 1;
        for(int i = 1; i < nums.length; i++){
            int currentdp = 1;
            for(int j = 0; j < i; j++){
                if( nums[i] > nums[j] && dp[j] + 1 > currentdp){
                    currentdp = dp[j] + 1;
                }else if( nums[i] <= nums[j] && dp[j] > currentdp){
                    currentdp = dp[j];
                }
            }
            if(currentdp > maxLength) maxLength = currentdp;
            dp[i] = currentdp;
        }
        return maxLength;
    }

算法思路二:

  • dp【i】表示长度为 i 的最长递增子序列(LIS)末尾的数 ,例如:{9,2,3,1,4,2}
  • 注意:这里dp的下标从1开始计算,不算0
  • 首先遍历 9, 则dp【1】 = 9,表示长度为1的最长子序列尾数为9
  • 之后遍历 2,
  • 由于 2 < dp【1】= 9, 所以2不可以和长度为1的最长子序列组成长度为2的最长子序列
  • 但是此时长度为1的最长递增子序列可以是9,也可以是2,但是最好的情况是2,所以这里将dp【1】 = 2(看完整个过程就知道为什么了)
  • 此时dp = {2}
  • 之后遍历 3
  • 由于3 > dp【1 】= 2,所以此时3可以和长度为1的最长子序列组成长度为2的最长子序列{2,3},所以将dp【2】 = 3。
  • 在这里就可以知道为什么上面要将9换成2了,因为后面的需要与前面的进行比较,当在多个子序列都可以的情况下,选择较小的!
  • 此时dp = {2,3}
  • 之后遍历1
  • 由于1 < dp【2】 = 3,所以1不可以和{2,3}组成长度为3的最长子序列,但是我们可以知道1应该是作为长度为1的最长子序列的最优解,所以再从头开始遍历dp,看看1可以替换哪里
  • 由于 1 < dp【1】 = 2,所以将dp【1】 = 1
  • 此时dp = {1,3}
  • 之后遍历4
  • 由于4 > dp【2】= 3,所以4可以加入到长度为2的最长子序列中,组成长度为3的最长子序列,并且以4作为尾数,所以dp【3】 = 4;
  • 此时dp = {1,3,4}
  • 最后遍历2
  • 由于2 < dp【3】 = 4,所以2也不能组成更长的子序列,但是2一定可以插入到前面的dp中作为最优解
  • 2 > dp【1】 = 1,不能插入
  • 2 < dp【2】 = 3,可以插入 ,则dp【2】 = 2,意思就是长度为2的递增子序列的尾数为2,这个2也是长度为2的递增子序列中尾数最小的
  • 此时dp = {1,2,4}
  • 最终结果为dp矩阵的最长长度3
  • 这里注意一个点,当我们判断该值无法新增子序列长度的时候,就要将其替换到dp矩阵中,而dp矩阵本身就是有序的,所以符合二分查找的思想,这样就直接将算法的复杂度降低了!
  • 所以该算法的时间复杂度为O(nlogn),这里我并没有进行算法优化,可以将那部分优化成logn的二分查找即可!
public static int maxQueue1(){
        //dp[i] 表示长度为 i 的最长递增子序列(LIS)末尾的数。
        int[] dp = new int[nums.length + 1];
        dp[1] = nums[0];
        int dpIndex = 1;
        for(int i = 1; i < nums.length - 1; i++){
            
            if(nums[i] > dp[dpIndex]){//可以组成更长的子序列
                dp[i] = dp[dpIndex] + 1;
                dpIndex++;
            }
            else {//进行dp矩阵的查找替换
                //这部分优化成logn 二分查找 因为是有序的
                for(int j = 1; j <= dpIndex; j++){
                    if(dp[j] > nums[i]) {
                        dp[j] = nums[i];
                        break;
                    }
                }
            }
        }

        return dpIndex;
    }

总结

  • 这道题自己其实是有想法的,知道每次要判断子问题的最长子序列的尾数,并且还要知道子问题的最长子序列的长度,但是一直都不知道一个dp矩阵如何去存储这两个数据,这是我的一个没有想到的点。知道要什么参数,但是不知道怎么实现。这一道题其实就是通过下标将两个参数联系了起来!
  • 第二个就是状态转移方程不一定是要F(n),F(n-1),F(n-2)的关系,甚至有可能会遍历之前所有的F(i)才可以知道F(n)的值,这也是我的另一个知识盲区,这一道题就是这样子,每次求F(n)的时候有时需要去遍历dp矩阵才可以。