leetcode 300. 最长递增子序列(动态规划 、贪心+二分查找)

334 阅读3分钟

最长递增子序列

题目

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

示例 1:

输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

示例 2:

输入:nums = [0,1,0,3,2,3]
输出:4

示例 3:

输入:nums = [7,7,7,7,7,7,7]
输出:1

提示:

  • 1 <= nums.length <= 2500
  • -104 <= nums[i] <= 104

解法

动态规划 O(n2)

定义dp[i]表示截止第i个数的最长递增子序列的个数。则dp[i+1]等于0-i中比nums[i+1]小的值(设为j)的 max(dp[i + 1], dp[j])。最后所求结果为dp数组中最大值

/**
 * 动态规划
 * @param {number[]} nums
 * @return {number}
 */
 var lengthOfLIS = function (nums) {
    // 默认每个值的最大递增子序列为1,因为包括自己
    const dp = new Array(nums.length).fill(1);
    let res = 1;
    for (let i = 1; i < nums.length; i++) {
        // 循环小于i的dp数组,找到比自己小的值
        for (let j = i - 1; j >= 0; j--) {
            if (nums[i] > nums[j]) {
                // dp[i]表示截止i位的最大的最长递增子序列的个数
                dp[i] = Math.max(dp[i], dp[j] + 1)
            }
        }
        res = Math.max(res, dp[i])
    }
    return res;
};

贪心+二分 O(logn)

我们要求最长递增子序列,就是要子序列的末尾尽量的小。我们可以维护一个新数组arr,该数组下标的每一位表示长度为下标的最长递增子序列的末尾值。然后不停的找到长度一样但是值比当前值小的末尾值进行替换。

我们用二分在arr中找到大于当前值的最小值。

比如 [10,9,2,5,3,7,101,18], 初始化arr = [10];

进9,长度为1的递增子序列目前末尾最小值就是9,arr = [9];

进2,长度为1的递增子序列目前末尾最小值是2,arr = [2];

进5,5比2大,arr = [2, 5];

进3,去arr中找到大于3的最小值,替换,此时长度为2的递增子序列的末尾最小值是3, 3比5好,因为3可以有更多可能性,比如4,5可以放到3后面,而5不行, arr = [2, 3];

进7,arr = [2, 3, 7];

进101,arr = [2, 3, 7, 101];

进18,替换掉101,arr = [2, 3, 7, 18];

注意

arr保存的并不是当前的递增子序列,而是每个位置长度的递增子序列的最优末尾值。 从中间替换比末尾小的值不会影响之前的递增子序列,因为后面比末尾值大的数肯定还是加到末尾之后,而中间的递增子序列又进行了优化。

/**
 * 贪心+二分
 * @param {number[]} nums
 * @return {number}
 */
var lengthOfLIS = function (nums) {
    let arr = [nums[0]], len = 1;
    for (let i = 1; i < nums.length; i++) {
        if (nums[i] > arr[len - 1]) {
            arr[len++] = nums[i];
            continue;
        }
        let idx = binary_search(arr, nums[i]);
        // 相等,不累计
        if (idx === -1) continue;
        // 这里只要维护递增子序列的最后一个数,这样后面来的数走到这步肯定比idx的值小,
        // 代表该递增子序列最后一个值比之前的小,
        // 这里替换掉原来的子序列就很划算,因为后面递增的可能性更多
        arr[idx] = nums[i];
    }
    return len;
};


var binary_search = function (arr, target) {
    let left = 0, right = arr.length - 1, mid;
    // 找到在arr中找到大于当前值的最小值
    while (left <= right) {
        mid = left + ((right - left) >> 1);
        if (arr[mid] > target) {
            right = mid - 1;
        } else if (arr[mid] < target) {
            left = mid + 1
        } else {
            //相等,忽略
            return -1;
        }
    }
    return left;
}