题目描述
给定一个整数数组 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],那么它前面的元素只能从下标 0 到 i - 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,等于没有把前面的最优长度接到当前元素前面。
可以记住这条主线:
定义结尾状态 -> 枚举合法前驱 -> 取最长前驱 -> 加上当前元素 -> 所有结尾取最大值