9

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],返回 4

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

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

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

动态规划法

复杂度

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

思路

由于这个最长上升序列不一定是连续的,对于每一个新加入的数,都有可能跟前面的序列构成一个较长的上升序列,或者跟后面的序列构成一个较长的上升序列。比如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就行了。同时,我们还要及时更新最大长度。

代码

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;
    }
}

比较好理解的版本

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;
}

}


ethannnli
858 声望360 粉丝