LeetCode 300. 最长递增子序列

题目描述

给定一个整数数组 nums,返回其中最长严格递增子序列的长度。

子序列可以删除数组中的部分元素,但不能改变剩余元素的相对顺序。

例如:

复制代码
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列可以是 [2,3,7,101],长度为 4。

思路:

一开始没有明确的切入点,所以先从最直观的 O(n^2) 动态规划方向入手。

关键不是直接问"整个数组的 LIS 是多少",而是先缩小问题:

复制代码
如果递增子序列必须以 nums[i] 结尾,那么最长长度是多少?

这个定义能把一个全局问题拆成每个位置的局部状态。

状态定义

定义:

复制代码
dfs(i):以 nums[i] 结尾的最长递增子序列长度

这里的重点是"以 nums[i] 结尾"。既然结尾已经固定为 nums[i],那么它前面的元素只能从下标 0i - 1 中选择。

如果某个 j 满足:

复制代码
j < i 且 nums[j] < nums[i]

说明 nums[i] 可以接到以 nums[j] 结尾的递增子序列后面。

状态转移

在所有能接到 nums[i] 前面的 j 中,选择最长的那个:

复制代码
dfs(i) = max({ dfs(j) + 1 | 0 <= j < i 且 nums[j] < nums[i] }, default = 1)

如果不存在这样的 j,说明 nums[i] 前面没有元素能接上它,此时只选 nums[i] 自己,长度为 1

代码中也可以把 +1 放到循环外面:

复制代码
res 先表示能接到 nums[i] 前面的最长长度
最后 res + 1 表示把 nums[i] 自己接上

手动模拟

nums = [2, 5] 为例,计算 dfs(1),也就是以 5 结尾的 LIS 长度:

复制代码
j = 0
nums[0] = 2 < nums[1] = 5
dfs(0) = 1
所以 dfs(1) = dfs(0) + 1 = 2

如果是 nums = [5, 2],计算 dfs(1) 时,前面的 5 不能接到 2 前面,所以只选 2 自己:

复制代码
dfs(1) = 1

再看 nums = [1, 4, 2, 3],计算以 3 结尾的状态:

复制代码
1 < 3,可以接,来自 dfs(0) = 1
4 < 3,不可以接
2 < 3,可以接,来自 dfs(2) = 2

dfs(3) = max(1, 2) + 1 = 3

对应的递增子序列是 [1, 2, 3]

记忆化搜索

直接递归会重复计算同一个 dfs(i)。例如后面的多个元素都可能尝试接在同一个下标后面,导致同一个状态被反复访问。

因此使用 memo[i] 缓存 dfs(i) 的结果。由于 LIS 长度至少为 1,所以 memo[i] = 0 可以表示"尚未计算"。

伪代码:

复制代码
函数 lengthOfLIS(nums):
    n <- nums.length
    memo <- 长度为 n 的数组,初始值为 0
    ans <- 0

    对 i 从 0 到 n - 1:
        ans <- max(ans, dfs(i))

    返回 ans

函数 dfs(i):
    如果 memo[i] > 0:
        返回 memo[i]

    res <- 0
    对 j 从 0 到 i - 1:
        如果 nums[j] < nums[i]:
            res <- max(res, dfs(j))

    res <- res + 1
    memo[i] <- res
    返回 res

对应 Java 代码:

复制代码
class Solution {
    public int lengthOfLIS(int[] nums) {
        int n = nums.length;
        int[] memo = new int[n];
        int ans = 0;

        for (int i = 0; i < n; i++) {
            ans = Math.max(ans, dfs(nums, i, memo));
        }

        return ans;
    }

    public int dfs(int[] nums, int i, int[] memo) {
        if (memo[i] > 0) return memo[i];

        int res = 0;
        for (int j = 0; j < i; j++) {
            if (nums[j] < nums[i]) {
                res = Math.max(res, dfs(nums, j, memo));
            }
        }

        res++;
        return memo[i] = res;
    }
}

易错点

1. 调用了 dfs(j),但没有使用返回值

如果只写递归调用,而不更新 res,循环里的计算结果就不会影响当前状态:

复制代码
找到合法 j 后,必须用 dfs(j) 更新当前能接上的最长长度

也就是核心逻辑必须是:

复制代码
res <- max(res, dfs(j))

建议测试用例

复制代码
nums = [2,5]                  -> 2
nums = [5,2]                  -> 1
nums = [1,4,2,3]              -> 3
nums = [10,9,2,5,3,7,101,18]  -> 4
nums = [7,7,7,7]              -> 1

复杂度分析

  • 时间复杂度:O(n^2)。每个 dfs(i) 只会真正计算一次,计算时会枚举它前面的所有 j
  • 空间复杂度:O(n)。主要来自 memo 数组和递归调用栈。

复盘

这题的关键是先固定"结尾位置"。当 dfs(i) 被定义为"以 nums[i] 结尾的 LIS 长度"后,转移就只需要向左找所有比 nums[i] 小的前驱。

本轮最重要的自检点是:递归函数的返回值要参与当前状态更新。只调用 dfs(j) 不更新 res,等于没有把前面的最优长度接到当前元素前面。

可以记住这条主线:

复制代码
定义结尾状态 -> 枚举合法前驱 -> 取最长前驱 -> 加上当前元素 -> 所有结尾取最大值
相关推荐
欧叶冲冲冲1 小时前
Python常见数据结构的CRUD(LeetCode高频版速查)
数据结构·python·leetcode
祖力551 小时前
Linux应用软件编程:目录IO与framebuffer
linux·运维·算法·framebuffer·目录io
名字还没想好☜1 小时前
Go 用 slices/maps 标准库泛型函数:告别手写 Contains、Sort、去重(Go 1.21)
开发语言·后端·算法·golang·go
地平线开发者1 小时前
【模型轻量化专题】深度学习模型为什么需要轻量化
算法
圣保罗的大教堂1 小时前
leetcode 3622. 判断整除性 简单
leetcode
_Narcissus_2 小时前
链表算法题和静态链表
数据结构·c++·笔记·算法·链表·ai·力扣
Lyyaoo.2 小时前
【普通数组】【中等】除了自身以外数组的乘积
数据结构·算法·leetcode
threerocks2 小时前
《The AI-Native SDLC Playbook》万字拆解
算法·aigc·ai编程
夏玉林的学习之路2 小时前
算法8.环形队列
算法