【算法】动态规划之最长递增子序列

157 阅读2分钟

什么是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] 来手动推导:

  1. dp[0] = 1(10 自己)

  2. dp[1] = 1(9 < 10,不接任何前面的,长度为1)

  3. dp[2] = 1(2 < 9, 2 < 10,不接,长度为1)

  4. dp[3] = 2(5 > 2,2 后面,dp[2] + 1 = 2)

  5. dp[4] = 2(3 > 2,接在 2 后面,dp[2] + 1 = 2)

  6. dp[5] = 3(7 > 2, 7 > 5, 7 > 3,取最大 dp[3] + 1 = 3)

  7. dp[6] = 4(101 > 所有前面的,取最大 dp[5] + 1 = 4)

  8. 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)