LeetCode 「中等」300.最长递增子序列

sankigan2025-2-27算法LeetCode

最长递增子序列open in new window

题目描述

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

示例

输入:nums = [10,9,2,5,3,7,101,18]
输出:4

输入:nums = [0,1,0,3,2,3]
输出:4

输入:nums = [7,7,7,7,7,7,7]
输出:1

解答

动态规划

动态规划是解决这个问题的直观方法。我们用一个数组 dp 来记录每个位置的最长递增子序列长度

  • 定义:dp[i] 表示以 nums[i] 结尾的最长递增子序列的长度
  • 初始值:每个 dp[i] 至少为 1,因为每个数字本身可以看作一个长度为 1 的递增子序列
  • 状态转移:对于每个 i,我们检查所有 j < i 的位置:
    • 如果 nums[j] < nums[i],说明 nums[i] 可以接在 nums[j] 后面形成一个更长的递增子序列
    • 因此,dp[i] = max(dp[i], dp[j] + 1)

最后,最长递增子序列的长度就是 dp 数组中的最大值

Code
/**
 * @param {number[]} nums
 * @return {number}
 */
var lengthOfLIS = function(nums) {
  if (!nums.length) return 0;

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

  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);
      }
    }
  }

  return Math.max(...dp);
};

二分查找

  1. 维护一个数组 tails,其中 tails[i] 表示长度为 i + 1 的递增子序列的最小尾部元素
  2. 遍历数组 nums,对于每个数字 num
  • 如果 num 大于 tails 最后一个元素,将其追加到 tails 末尾
  • 否则,用二分查找找到 tails 中第一个大于等于 num 的位置,并用 num 替换该位置的值
  1. 最终,tails 的长度即为最长递增子序列的长度

为什么是 “最小尾部元素”?

这个定义的核心目的是为了确保我们能够高效地更新和维护递增子序列的尾部元素,从而找到最长的递增子序列。具体来说:

  • 如果 tails[i] 是长度为 i + 1 的递增子序列的最小尾部元素,那么任何比 tails[i] 大的数字都可以接在长度为 i + 1 的递增子序列后面,形成一个长度为 i + 2 的递增子序列
  • 通过维护最小尾部元素,我们能够确保在更新 tails 时,总是能够找到一个更优的递增子序列
Code
var biSearch = function (nums, target) {
  let left = 0, right = nums.length - 1;
  while (left <= right) {
    const mid = Math.floor((left + right) / 2);
    if (nums[mid] === target) {
      return mid;
    } else if (nums[mid] > target) {
      right = mid - 1;
    } else {
      left = mid + 1;
    }
  }
  return left;
};

var lengthOfLIS = function(nums) {
  const tails = [];
  nums.forEach(num => {
    if (!tails.length || num > tails[tails.length - 1]) {
      tails.push(num);
    } else {
      const targetIdx = biSearch(tails, num);
      tails[targetIdx] = num;
    }
  });
  return tails.length;
};

总结

  • 替换操作是为了保持 tails 数组的 “最优性”,即每个位置的尾部元素尽可能小
  • 直接添加会导致 tails 数组失去 “最优性”,并且后续数字无法接在后面
  • 替换操作能够确保 tails 数组始终保持递增性,并为后续数字提供更多的机会来延长递增子序列
更新于 2025/3/20 23:23:29