跳到主要内容

300. 最长递增子序列

题目描述

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

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

示例 1:

输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

示例 2:

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

示例 3:

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

提示:

  • 1 <= nums.length <= 2500
  • -10^4 <= nums[i] <= 10^4

进阶:

  • 你能将算法的时间复杂度降低到 O(n log n) 吗?

解题思路

第一步:理解问题本质

最长递增子序列(LIS, Longest Increasing Subsequence)是经典的动态规划问题。

关键理解:

  • 子序列不要求连续,但要求相对顺序不变
  • "严格递增"意味着后面的数必须大于前面的数

第二步:暴力解法

枚举所有子序列,检查是否为递增序列,记录最大长度。

def lengthOfLIS_brute(nums):
n = len(nums)
max_len = 0

# 枚举所有子集(用位运算)
for mask in range(1 << n):
subseq = []
for i in range(n):
if mask & (1 << i):
subseq.append(nums[i])

# 检查是否为递增序列
is_increasing = True
for i in range(1, len(subseq)):
if subseq[i] <= subseq[i-1]:
is_increasing = False
break

if is_increasing:
max_len = max(max_len, len(subseq))

return max_len

为什么不够好?

  • 时间复杂度 O(2^n * n),会超时
  • 没有利用子问题的重叠性质

第三步:优化解法 - 动态规划 O(n^2)

定义状态: dp[i] 表示以 nums[i] 结尾的最长递增子序列长度

状态转移:

dp[i] = max(dp[j] + 1) 对于所有 j < i 且 nums[j] < nums[i]

初始化: dp[i] = 1(每个元素自身构成长度为 1 的序列)

第四步:最优解法 - 贪心 + 二分查找 O(n log n)

核心思想:

  • 维护一个数组 tailstails[i] 表示长度为 i+1 的递增子序列的最小末尾元素
  • 对于每个新元素,用二分查找找到它应该替换的位置
  • 如果比所有 tails 元素都大,就扩展序列长度

为什么这样有效?

  • 更小的末尾元素更有利于后续扩展
  • tails 数组始终保持有序,可以用二分查找

完整代码实现

from typing import List
import bisect


class Solution:
"""
300. 最长递增子序列 (LIS) - 动态规划 + 贪心优化

问题定义:
找到数组中最长的严格递增子序列的长度。
子序列不要求连续,但要求相对顺序不变。

解法一:动态规划 O(n^2)
- dp[i] 表示以 nums[i] 结尾的最长递增子序列长度
- 转移方程:dp[i] = max(dp[j] + 1) 对于所有 j < i 且 nums[j] < nums[i]
- 初始:dp[i] = 1(每个元素自身构成长度为 1 的序列)

解法二:贪心 + 二分查找 O(n log n)
- 维护一个数组 tails,tails[i] 表示长度为 i+1 的递增子序列的最小末尾元素
- 对于每个新元素,用二分查找找到它应该替换的位置
- 如果比所有 tails 元素都大,就扩展序列长度

时间复杂度:
- DP: O(n^2)
- 贪心+二分: O(n log n)

空间复杂度: O(n) - DP数组或tails数组
"""

def lengthOfLIS(self, nums: List[int]) -> int:
"""
动态规划解法 O(n^2)
dp[i] 表示以 nums[i] 结尾的最长递增子序列长度
"""
if not nums:
return 0

n = len(nums)
# dp[i] 表示以 nums[i] 结尾的最长递增子序列长度
dp = [1] * n

for i in range(n):
# 检查所有在 i 之前的元素 j
for j in range(i):
if nums[i] > nums[j]:
# 如果 nums[i] 可以接在 nums[j] 后面,更新 dp[i]
dp[i] = max(dp[i], dp[j] + 1)

# 返回所有 dp 值中的最大值
return max(dp)

def lengthOfLIS_greedy(self, nums: List[int]) -> int:
"""
贪心 + 二分查找解法 O(n log n)
tails[i] 表示长度为 i+1 的递增子序列的最小末尾元素
"""
if not nums:
return 0

tails = [] # tails[i] = 长度为 i+1 的递增子序列的最小末尾元素

for num in nums:
# 在 tails 中找到第一个 >= num 的位置
idx = bisect.bisect_left(tails, num)

if idx == len(tails):
# num 比所有 tails 元素都大,可以扩展序列
tails.append(num)
else:
# 替换 tails[idx] 为 num,保持更小的末尾元素
tails[idx] = num

return len(tails)

示例推演

nums = [10, 9, 2, 5, 3, 7, 101, 18] 为例:

动态规划法:

inums[i]jnums[j]nums[i]>nums[j]?dp[j]+1dp[i]
010----1
19010-1
220,110,9-1
350,1,210,9,2是(j=2)22
430,1,2,310,9,2,5是(j=2)22
57......是(j=3,4)3,33
6101......是(所有)最大44
718......是(j=3,4,5)3,3,44

最终结果:max(dp) = 4

贪心 + 二分查找法:

num=10: tails = [10]
num=9: tails = [9] (替换 10)
num=2: tails = [2] (替换 9)
num=5: tails = [2, 5] (5 > 2,扩展)
num=3: tails = [2, 3] (替换 5)
num=7: tails = [2, 3, 7] (7 > 3,扩展)
num=101: tails = [2, 3, 7, 101] (扩展)
num=18: tails = [2, 3, 7, 18] (替换 101)

结果: len(tails) = 4

关键观察:

  • tails 数组始终保持有序
  • 替换操作保持了"相同长度下末尾元素最小"的性质
  • 最终 tails 的长度就是 LIS 的长度

复杂度分析

解法时间复杂度空间复杂度说明
暴力O(2^n)O(n)枚举所有子集
动态规划O(n^2)O(n)经典 DP 解法
贪心+二分O(n log n)O(n)最优解法

易错点总结

1. 严格递增 vs 非严格递增

严格递增(本题):

if nums[i] > nums[j]:  # 严格大于
dp[i] = max(dp[i], dp[j] + 1)

非严格递增(允许相等):

if nums[i] >= nums[j]:  # 大于等于
dp[i] = max(dp[i], dp[j] + 1)

2. 二分查找的选择

正确做法: 使用 bisect_left 找第一个 >= num 的位置。

idx = bisect.bisect_left(tails, num)

错误做法: 使用 bisect_right 会导致错误。

3. 返回的是长度而非序列

tails 数组本身不一定是 LIS,只是用来计算长度的工具。

例如:nums = [3, 1, 2],最终 tails = [1, 2],但 [1, 2] 是 LIS。

nums = [1, 2, 3, 0],最终 tails = [0, 2, 3][0, 2, 3] 不是原数组的子序列。

4. 边界条件

if not nums:
return 0

扩展思考

1. 相关题目

2. 如何输出具体的 LIS?

使用 DP 方法,记录前驱节点:

def lengthOfLIS_with_path(nums):
n = len(nums)
dp = [1] * n
prev = [-1] * n # 记录前驱

for i in range(n):
for j in range(i):
if nums[i] > nums[j] and dp[i] < dp[j] + 1:
dp[i] = dp[j] + 1
prev[i] = j

# 找到最大值的索引
max_idx = max(range(n), key=lambda i: dp[i])

# 重建路径
path = []
while max_idx != -1:
path.append(nums[max_idx])
max_idx = prev[max_idx]

return path[::-1] # 反转

3. 二维 LIS - 俄罗斯套娃信封

问题:信封 (w, h),一个信封能放入另一个当且仅当 w 和 h 都更小。

解法:

  1. 按宽度升序排序,宽度相同按高度降序排序
  2. 对高度求 LIS
def maxEnvelopes(envelopes):
# 按宽度升序,宽度相同按高度降序
envelopes.sort(key=lambda x: (x[0], -x[1]))
# 对高度求 LIS
heights = [h for _, h in envelopes]
return lengthOfLIS_greedy(heights)

4. 最长递减子序列

将数组取反,求 LIS 即可:

def lengthOfLDS(nums):
return lengthOfLIS([-x for x in nums])
加载评论中...