LeetCode题解 300 最长上升子序列

1. 题目描述

给定一个数字序列,求其最长上升子序列

1.1. 测试用例

测试用例
int[] nums = {4,2,4,5,3,7};

预期结果
4, 序列是{2,4,5,7}

1.2. 函数签名

public int lengthOfLIS(int[] nums){

}

2. 题解

2.1. 动态规划解法

时间复杂度为O(N^2)

2.1.1. 分析

  • 确定状态:dp[i]以nums[i]结尾的最长子序列的长度
  • 转移方程: $dp[i] = max { 1, dp[j] + 1 } \quad 0 \le j < i 且 nums[i] \ge nums[j]$
    • 以nums[i]结尾的LIS,序列中nums[i]的上一个数字可能是nums[i]之前的任何一个比它小的数, 假设是上一个数字nums[j], 则有j < i 且 nums[i] $\le$ ge nums[j], 此时的序列的长度为dp[j] + 1
    • 也有可能nums[i]之前的数字没有比它小的,那么以它结尾的LIS长度就是1
  • 返回值。注意要返回dp数组中的最大值,而不是dp[n]

2.1.2. Java实现

public int longestIncreasingSubsequence(int[] nums) {
    if(nums == null || nums.length == 0){
        return 0;
    }
    //dp[i] : 以nums[i]结尾的LIS的长度
    int n = nums.length;
    int[] dp = new int[n] ;
    //初始化
    Arrays.fill(dp, 1);
    for(int i = 0; i < n; i++){
        //nums[i]之前的每一个数字
        for(int j = 0; j < i; j++){
            if(nums[i] > nums[j]){
                dp[i] = Math.max(dp[i], dp[j] + 1);
            }
        }
    }
    //dp中的最大值
    int res = dp[0];
    for (int i = 1; i < n; i++) {
        res = Math.max(res, dp[i]);
    }
    return res;
}

2.2. 二分解法

时间复杂度为O(N*logN),单独去理解比较难,在动态规划的解法上进一步思考会更好理解

2.2.1. 分析

数组{4, 2, 4, 5, 2,7}, 用DP解法可以得到,dp = {1, 1, 2, 3, 2,7}。假设计算dp[3], DP解法在得到dp[3]时,需要顺序查找3之前的dp值中可用的(序列尾元素小于nums[3]的)最大值,其实可以使用二分查找。但想使用二分查找前,先要证明一些结论。

通过dp数组我们知道dp[3]前面的dp有:

  • dp[0],dp[1]对应长度为1的序列{4}、{2},它们中最小的尾元素是2
  • dp[2]对应长度为2的序列{*, 4}, 它们中最小的尾元素是4

结论1 : 数值相等的那些dp只需要查找最小的尾元素是否可用

  • 分析:如果尾元素大的可用,尾元素小的一定可用,但是反过来不一定,所以最后的采用的一定会是dp相同的尾元素最小的其中一个

结论2 : dp值递增时,它们中最小的尾元素肯定也是递增的

  • 反证法,dp = len1的最小尾元素是t1,dp = len2的最小尾元素是t2, len1 < len2。如果t1 > t2 , 因为长为len2的序列为...t2, 其中肯定有长为len1的序列 ...t3...t2, t3<=t2, 和长为len1的序列的最小尾元素t1 > t2矛盾

有了这两个结论,可以用tail[j]记录dp值为j的序列们的最小尾元素,dp[i]的值可以通过对tail进行二分查找可用的尾元素值来得到

2.2.2. Java实现

public int longestIncreasingSubsequence(int[] nums) {
    int n = nums.length;
    //minTail[i] 所有长度为i的子序列中最小的尾元素的值
    int[] minTail = new int[n + 1];
    minTail[0] = Integer.MIN_VALUE;
    //最后一个记录的位置
    int maxLen = 0;
    for (int i = 0; i < n; i++) {
        int prePlace = 0;
        int start = 0, end = maxLen, mid;
        while (start <= end) {
            mid = start + (end - start) / 2;
            //nums[i]会放到最后一个比它小的堆的右边
            if (nums[i] > minTail[mid]) {
                prePlace = mid;
                start = mid + 1;
            } else {
                end = mid - 1;
            }
        }
        //更新nums[i]放的堆的最小值
        minTail[prePlace + 1] = nums[i];
        //所有的堆的最小值都比它小,另起一堆
        if (prePlace + 1 > maxLen) {
            maxLen = prePlace + 1;
        }
    }
    return maxLen;
}
posted @ 2020-06-11 21:52  Leonuoa7  阅读(182)  评论(0编辑  收藏  举报