300. 最长递增子序列 (longest increasing subsequence)

3,816 阅读2分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第21天,点击查看活动详情

300. 最长递增子序列 题目描述:给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。

子序列是字符元素不一定连续,但相对位置一致的序列。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

示例1示例2示例3
输入nums = [10,9,2,5,3,7,101,18]
输出44
解释:最长递增子序列是 [2,3,7,101],因此长度为 44
输入nums = [0,1,0,3,2,3]
输出44
输入nums = [7,7,7,7,7,7,7]
输出11

中规中矩的动态规划

本题与 # 674. 最长连续递增序列 仅差 “连续” 二字,但是整体的解题思路不变。

1、确定 dp 状态数组

依然定义 dp[i]dp[i] 是以 num[i]num[i] 结尾的严格递增子序列的长度,其中 i[0,n),n=nums.lengthi \in [0,n),n=nums.length

2、确定 dp 状态方程

既然子序列可以不连续,当我们遍历到 nums[i]nums[i] 时,不能仅仅判断 nums[i]nums[i]nums[i1]nums[i - 1],还额外需要另外一维状态,即 nums[j]nums[j],其中 j[0,i)j \in[0, i)

  • nums[i]>nums[j]nums[i] \gt nums[j] 时,符合严格递增, 此时存在 dp[i]=dp[j]+1dp[i] = dp[j] + 1

  • nums[i]nums[j]nums[i] \le nums[j] 时,直接忽略即可。

NOTE: 每次遍历 jj 时需要对 dp[i]dp[i] 取最大值,即 dp[i]=max(dp[i],dp[j]+1)dp[i] = max(dp[i], dp[j] + 1)

3、确定 dp 初始状态

dpdp 初始化的每个元素为 11(非严格增序数列的最长递增序列为 11);

4、确定遍历顺序

  • 外层循环从 i=1i = 1 遍历到 i=n1i = n - 1

  • 内层循环从 j=0j = 0 遍历到 j=i1j = i - 1

5、确定最终返回值

回归到 dpdp 定义中,dp[n1]dp[n - 1] 仅代表以 n1n-1 结尾的最长子序列的长度,并不是全局最长子序列的长度,应该从 dpdp 数组中选择最大值,即 max(...dp)max(...dp)

6、代码示例

/**
 * 空间复杂度 O(n)
 * 时间复杂度 O(n^2)
 */
function lengthOfLIS(nums: number[]): number {
    const n = nums.length;
    const dp = new Array(n).fill(1);

    for (let i = 1; i < n; i++) {
        for (let j = 0; j < i; j++) {
            if (nums[i] > nums[j]) {
                dp[i] = Math.max(dp[i], dp[j] + 1);
            }
        }
    }

    return Math.max(...dp);
};
/**
 * 空间复杂度 O(n)
 * 时间复杂度 O(n^2)
 */
function lengthOfLIS(nums: number[]): number {
    const n = nums.length;
    const dp = new Array(n).fill(1);
    let ans = 1;

    for (let i = 1; i < n; i++) {
        for (let j = 0; j < i; j++) {
            if (nums[i] > nums[j]) {
                dp[i] = Math.max(dp[i], dp[j] + 1);
            }
        }
        ans = Math.max(ans, dp[i])
    }

    return ans;
};

参考

# 重识动态规划