什么是LIS?
最长递增子序列是指在一个给定的序列中,找到一个子序列(不要求连续),使得这个子序列的元素是严格递增的,并且长度尽可能长。
示例
假设有一个序列:[10, 9, 2, 5, 3, 7, 101, 18]
可能的递增子序列有:
- [10](注意,每个单独的数字也是子序列,如其他的 [10]、[9]...)
- [2, 5, 7, 101]
- [2, 3, 7, 18]
- 其中最长的递增子序列长度是4,比如 [2, 5, 7, 101] 或 [2, 3, 7, 18]。
动态规划解决LIS
动态规划的核心思想是把大问题分解成小问题,通过记录子问题的解来构建最终答案。我们来一步步推导。
定义状态
我们用一个数组 dp[i] 来表示以第 i 个元素结尾的最长递增子序列的长度。
- 比如对于序列 [10, 9, 2, 5, 3, 7, 101, 18],dp[i] 的含义是以 arr[i] 结尾的 LIS 长度。
状态转移方程
对于每个位置 i,我们需要检查它之前的所有位置 j(j < i),如果 arr[j] < arr[i],说明 arr[j] 可以接到 arr[i] 前面构成递增序列。此时,dp[i] 可以从 dp[j] + 1 中取最大值。
公式:由于是递增,前面的数 arr[j] 必须比后面的 arr[i] 小
- dp[i] = max(dp[j] + 1),其中 j < i 且 arr[j] < arr[i]。
- 如果没有满足条件的 j,则 dp[i] = 1(只有自己一个元素)。
初始化
每个位置的最小 LIS 长度是1,因为单个元素本身就是一个递增子序列。
计算过程
我们用上面的例子 [10, 9, 2, 5, 3, 7, 101, 18] 来手动推导:
-
dp[0] = 1(10 自己)
-
dp[1] = 1(9 < 10,不接任何前面的,长度为1)
-
dp[2] = 1(2 < 9, 2 < 10,不接,长度为1)
-
dp[3] = 2(5 > 2,2 后面,dp[2] + 1 = 2)
-
dp[4] = 2(3 > 2,接在 2 后面,dp[2] + 1 = 2)
-
dp[5] = 3(7 > 2, 7 > 5, 7 > 3,取最大 dp[3] + 1 = 3)
-
dp[6] = 4(101 > 所有前面的,取最大 dp[5] + 1 = 4)
-
dp[7] = 4(18 > 2, 3, 5, 7,取最大 dp[5] + 1 = 4)
最终 dp 数组为:[1, 1, 1, 2, 2, 3, 4, 4]。
LIS 的长度是 dp 中的最大值,即 4。
代码示例
知道了状态转移方程,代码实现起来就很简单
function lengthOfLIS(arr: number[]): number {
if (!arr.length) return 0; // 空数组返回 0
const n: number = arr.length;
const dp: number[] = new Array(n).fill(1); // 初始化 dp 数组,每个位置长度为 1
for (let i = 1; i < n; i++) {
for (let j = 0; j < i; j++) {
if (arr[i] > arr[j]) {
dp[i] = Math.max(dp[i], dp[j] + 1); // 更新 dp[i]
}
}
}
return Math.max(...dp); // 返回 dp 中的最大值
}
// 测试
const arr: number[] = [10, 9, 2, 5, 3, 7, 101, 18];
console.log(lengthOfLIS(arr)); // 输出 4
时间复杂度: O(n) 空间复杂度: O(n)