最长递增子序列
问题描述:
- 给定一个序列,求解其中长度最长的递增子序列,最长递增子序列表示必须递增但是可以位置不连续的序列。
- 例如:{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矩阵才可以。