300.最长递增子序列

LeetCode 原题链接 题目

1. dp 数组含义

定义一维数组 dp

dp[i] 表示以 nums[i] 结尾的最长严格递增子序列长度。

注意这里的关键是“以 nums[i] 结尾”。

因为子序列不要求连续,所以不能只看相邻元素,而是要看 i 前面的所有位置 j。只要满足:

nums[j] < nums[i]

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

题目要求整个数组中的最长递增子序列长度,所以最终答案是:

Math.max(...dp)

2. 确定状态转移方程

对于位置 i,需要枚举它前面的每一个位置 j

0 <= j < i

如果:

nums[j] < nums[i]

说明 nums[i] 可以接在 nums[j] 后面,形成一个更长的递增子序列:

dp[j] + 1

但是满足条件的 j 可能有很多个,我们要选择其中最长的那个,所以状态转移方程是:

dp[i] = Math.max(dp[i], dp[j] + 1);

一句话总结:以 nums[i] 结尾的最长递增子序列,可以由前面某个比它小的 nums[j] 接上 nums[i] 得到。

3. dp 数组如何初始化

每个位置本身都可以单独组成一个长度为 1 的递增子序列。

所以所有 dp[i] 都初始化为 1

const dp = new Array(nums.length).fill(1);

例如 nums = [10, 9, 2],即使没有任何两个数字能组成递增关系,每个数字自己也至少是一个长度为 1 的递增子序列。

如果 nums 是空数组,直接返回 0

4. 确定遍历顺序

dp[i] 依赖的是它前面位置的状态 dp[j],其中:

j < i

所以外层循环 i 必须从前往后遍历。

对于每个 i,内层循环枚举 0i - 1 的所有位置 j

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

这样在计算 dp[i] 时,所有可能用到的 dp[j] 都已经计算完成。

5. 举例打印 dp 数组

nums = [10, 9, 2, 5, 3, 7] 为例。

初始化:

下标012345
nums1092537
dp111111

从左到右遍历:

i = 0nums[i] = 10

前面没有元素,dp[0] 保持为 1

dp = [1, 1, 1, 1, 1, 1]

i = 1nums[i] = 9

前面只有 10,但 10 < 9 不成立,不能接在后面。

dp = [1, 1, 1, 1, 1, 1]

i = 2nums[i] = 2

前面的 109 都比 2 大,不能接在后面。

dp = [1, 1, 1, 1, 1, 1]

i = 3nums[i] = 5

2 < 5,所以 5 可以接在 2 后面:

dp[3] = dp[2] + 1 = 2
dp = [1, 1, 1, 2, 1, 1]

i = 4nums[i] = 3

2 < 3,所以 3 可以接在 2 后面:

dp[4] = dp[2] + 1 = 2
dp = [1, 1, 1, 2, 2, 1]

i = 5nums[i] = 7

2 < 7,可以得到长度 2

5 < 7,可以得到:

dp[3] + 1 = 3

3 < 7,也可以得到:

dp[4] + 1 = 3

所以:

dp[5] = 3

最终:

dp = [1, 1, 1, 2, 2, 3]

最长递增子序列长度是:

Math.max(...dp) = 3

对应的递增子序列可以是:

[2, 5, 7]

也可以是:

[2, 3, 7]

6. 代码实现

/**
 * @param {number[]} nums
 * @return {number} 最长严格递增子序列的长度
 */
var lengthOfLIS = function (nums) {
  // 空数组没有子序列,长度为 0;也避免对空 dp 求最大值得到 -Infinity
  if (nums.length === 0) return 0;

  // dp[i]:必须以 nums[i] 结尾的最长严格递增子序列长度
  // 每个元素都能单独组成长度为 1 的子序列,因此全部初始化为 1
  const dp = new Array(nums.length).fill(1);

  // 从左到右确定每个结尾的最优长度,保证用到的 dp[j] 已经计算完成
  for (let i = 0; i < nums.length; i++) {
    // 枚举当前元素前面的所有位置,而不只是相邻位置:子序列可以不连续
    // j < i 保证选出的元素保持原数组顺序;i = 0 时内层不执行,dp[0] 保持 1
    for (let j = 0; j < i; j++) {
      // 只有 nums[j] 严格小于 nums[i],才能把 nums[i] 接在它后面
      // 相等时不能接,例如 [2, 2] 的最长严格递增子序列长度仍是 1
      if (nums[j] < nums[i]) {
        // dp[j] + 1:在以 nums[j] 结尾的最优子序列后接上当前元素
        // 可接的位置 j 可能有多个,取最大值,避免较短候选覆盖已有结果
        dp[i] = Math.max(dp[i], dp[j] + 1);
      }
    }
    // 内层结束后,dp[i] 已确定;若前面没有更小的元素,它仍为 1
  }

  // 最长子序列不一定以最后一个元素结尾,所以取所有 dp[i] 的最大值
  // 例如 [1, 2, 3, 0] 的 dp 为 [1, 2, 3, 1],答案是 3,而非最后一项 1
  return Math.max(...dp);
};

复杂度分析

  • 时间复杂度:O(n^2),每个位置 i 都要枚举它前面的所有位置 j
  • 空间复杂度:O(n),需要一个长度为 ndp 数组。