[Lintcode] Longest Increasing Subsequence 最长上升序列

Longest Increasing Subsequence

本文最新版本位于 https://yanjia.me/zh/2018/11/...

给定一个整数序列,找到最长上升子序列(LIS),返回LIS的长度。

样例 给出[5,4,1,2,3],这个LIS是[1,2,3],返回 3数组

给出[4,2,4,5,3,7],这个LIS是[4,4,5,7],返回 4code

挑战 要求时间复杂度为O(n^2) 或者O(nlogn)排序

说明 最长上升子序列的定义:ip

最长上升子序列问题是在一个无序的给定序列中找到一个尽量长的由低到高排列的子序列,这种子序列不必定是连续的或者惟一的。
https://en.wikipedia.org/wiki...get

动态规划法

复杂度

时间 O(N^2) 空间 O(N)it

思路

因为这个最长上升序列不必定是连续的,对于每个新加入的数,都有可能跟前面的序列构成一个较长的上升序列,或者跟后面的序列构成一个较长的上升序列。好比1,3,5,2,8,4,6,对于6来讲,能够构成1,3,5,6,也能够构成2,4,6。由于前面那个序列长为4,后面的长为3,因此咱们更愿意6组成那个长为4的序列,因此对于6来讲,它组成序列的长度,其实是以前最长一个升序序列长度加1,注意这个最长的序列的末尾是要小于6的,否则咱们就把1,3,5,8,6这样的序列给算进来了。这样,咱们的递推关系就隐约出来了,假设dp[i]表明加入第i个数能构成的最长升序序列长度,咱们就是要在dp[0]dp[i-1]中找到一个最长的升序序列长度,又保证序列尾值nums[j]小于nums[i],而后把这个长度加上1就好了。同时,咱们还要及时更新最大长度。io

代码

public class Solution {
    public int longestIncreasingSubsequence(int[] nums) {
        // write your code here
        if(nums.length == 0){
            return 0;
        }
        // 构建最长升序序列长度的数组
        int[] lis = new int[nums.length];
        lis[0] = 1;
        int max = 0;
        for (int i = 1; i < nums.length; i++){
            // 找到dp[0]到dp[i-1]中最大的升序序列长度且nums[j]<nums[i]
            for (int j = 0; j < i; j++){
                if (nums[j] <= nums[i]){
                    lis[i] = Math.max(lis[i], lis[j]);
                }
            }
            // 加1就是该位置能构成的最长升序序列长度
            lis[i] += 1;
            // 更新全局长度
            max = Math.max(max, lis[i]);
        }
        return max;
    }
}

比较好理解的版本class

public class Solution {
    public int longestIncreasingSubsequence(int[] nums) {
        if(nums.length == 0){
            return 0;
        }
        int[] lis = new int[nums.length];
        int max = 0;
        for (int i = 0; i < nums.length; i++){
            int localMax = 0;
            // 找出当前点以前的最大上升序列长度
            for (int j = 0; j < i; j++){
                if (lis[j] > localMax && nums[j] <= nums[i]){
                    localMax = lis[j];
                }
            }
            // 当前点则是该局部最大上升长度加1
            lis[i] = localMax + 1;
            // 用当前点的长度更新全局最大长度
            max = Math.max(max, lis[i]);
        }
        return max;
    }
}

耐心排序法

复杂度

时间 O(NlogN) 空间 O(N)搜索

思路

1,3,5,2,8,4,6这个例子中,当到6时,咱们一共能够有四种
(1)不一样长度
(2)且保证该升序序列在同长度升序序列中末尾最小
的升序序列集合

1
1,2
1,3,4
1,3,5,6

这些序列都是将来有可能成为最长序列的候选人。这样,每来一个新的数,咱们便按照如下规则更新这些序列

  • 若是nums[i]比全部序列的末尾都大,或等于最大末尾,说明有一个新的不一样长度序列产生,咱们把最长的序列复制一个,并加上这个nums[i]
  • 若是nums[i]比全部序列的末尾都小,说明长度为1的序列能够更新了,更新为这个更小的末尾。
  • 若是在中间,则更新那个末尾数字刚刚大于等于本身的那个序列,说明那个长度的序列能够更新了。

好比这时,若是再来一个9,那就是第三种状况,更新序列为

1
1,2
1,3,4
1,3,5,6
1,3,5,6,9

若是再来一个3,那就是第二种状况,更新序列为

1
1,2
1,3,3
1,3,5,6

若是再来一个0,那就是第一种状况,更新序列为

0
1,2
1,3,3
1,3,5,6

前两种都很好处理,O(1)就能解决,主要是第三种状况,实际上咱们观察直到6以前这四个不一样长度的升序序列,他们末尾是递增的,因此能够用二分搜索来找到适合的更新位置。

注意

  • 二分搜索时若是在tails数组中,找到咱们要插入的数,也直接返回那个结尾的下标,虽然这时候更新这个结尾没有意义,但少了些判断简化了逻辑

代码

public class Solution {
public int longestIncreasingSubsequence(int[] nums) {
    // write your code here
    if(nums.length == 0){
        return 0;
    }
    // len表示当前最长的升序序列长度(为了方便操做tails咱们减1)
    int len = 0;
    // tails[i]表示长度为i的升序序列其末尾的数字
    int[] tails = new int[nums.length];
    tails[0] = nums[0];
    // 根据三种状况更新不一样升序序列的集合
    for(int i = 1; i < nums.length; i++){
        if(nums[i] < tails[0]){
            tails[0] = nums[i];
        } else if (nums[i] >= tails[len]){
            tails[++len] = nums[i];
        } else {
        // 若是在中间,则二分搜索
            tails[binarySearch(tails, 0, len, nums[i])] = nums[i];
        }
    }
    return len + 1;
}

private int binarySearch(int[] tails, int min, int max, int target){
    while(min <= max){
        int mid = min + (max - min) / 2;
        if(tails[mid] == target){
            return mid;
        }
        if(tails[mid] < target){
            min = mid + 1;
        }
        if(tails[mid] > target){
            max = mid - 1;
        }
    }
    return min;
}

}

相关文章
相关标签/搜索