1. 最长上升子序列问题概述
最长上升子序列(Longest Increasing Subsequence,简称LIS)是动态规划领域最经典的入门问题之一。我第一次接触这个问题是在准备算法面试时,当时被它简洁的定义和巧妙的解法深深吸引。给定一个无序的整数序列,我们需要找到其中最长的严格递增子序列的长度。这里的子序列不要求连续,但必须保持原始顺序。
举个例子,对于序列[10, 9, 2, 5, 3, 7, 101, 18],最长的上升子序列是[2, 3, 7, 101],长度为4。这个问题看似简单,但蕴含着动态规划思想的精髓,也是许多复杂问题的基础。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 暴力解法与问题分析
2.1 穷举法的局限性
最直观的解法是枚举所有可能的子序列,然后检查它们是否是上升的。对于一个长度为n的序列,子序列的数量是2^n,这意味着时间复杂度是指数级的。即使对于n=30的情况,也需要处理超过10亿个子序列,这在实践中是完全不可行的。
python复制def lis_brute_force(nums):
max_len = 0
from itertools import combinations
for l in range(1, len(nums)+1):
for sub in combinations(nums, l):
if all(sub[i] < sub[i+1] for i in range(len(sub)-1)):
max_len = max(max_len, len(sub))
return max_len
2.2 问题的最优子结构
动态规划之所以能高效解决这个问题,关键在于它具备最优子结构性质。如果我们知道前i个元素的最长上升子序列长度,那么前i+1个元素的解可以通过比较nums[i+1]与前i个元素的关系来推导。这种"分阶段决策"和"状态转移"正是动态规划的核心特征。
3. 动态规划解法详解
3.1 状态定义与初始化
我们定义dp[i]表示以第i个数字结尾的最长上升子序列的长度。初始时,每个元素本身就是一个长度为1的子序列,因此dp数组初始化为全1。
python复制def lengthOfLIS(nums):
if not nums:
return 0
dp = [1] * len(nums)
# 后续代码将填充这里
3.2 状态转移方程
对于每个位置i,我们需要检查所有j < i的元素:
- 如果nums[j] < nums[i],说明nums[i]可以接在nums[j]后面形成更长的子序列
- 我们取所有满足条件的j中dp[j]+1的最大值作为dp[i]
状态转移方程为:
dp[i] = max(dp[j] + 1 for j in range(i) if nums[j] < nums[i])
python复制for i in range(1, len(nums)):
for j in range(i):
if nums[j] < nums[i]:
dp[i] = max(dp[i], dp[j] + 1)
3.3 复杂度分析
这种方法的时间复杂度是O(n^2),因为对于每个元素i,我们需要遍历它之前的所有元素j。空间复杂度是O(n),只需要存储dp数组。相比暴力解法的指数复杂度,这是一个巨大的改进。
4. 算法优化与进阶思路
4.1 贪心+二分查找优化
更高效的O(nlogn)解法利用了贪心思想和二分查找。我们维护一个tails数组,其中tails[i]表示长度为i+1的所有上升子序列中最小的末尾元素。通过二分查找确定每个元素应该插入的位置,可以显著降低时间复杂度。
python复制import bisect
def lengthOfLIS(nums):
tails = []
for num in nums:
idx = bisect.bisect_left(tails, num)
if idx == len(tails):
tails.append(num)
else:
tails[idx] = num
return len(tails)
4.2 实际应用中的变种
最长上升子序列问题有许多变种,比如:
- 非严格递增(允许相等)
- 二维LIS问题(如信封嵌套问题)
- 带权值的LIS(求最大权值和而非最长长度)
理解基础解法后,这些变种都可以通过适当调整状态定义和转移方程来解决。
5. 实战技巧与常见错误
5.1 边界条件处理
在实际编码中,有几个边界条件需要特别注意:
- 空输入的情况
- 所有元素相同的情况
- 完全递减的序列(此时LIS长度为1)
5.2 调试技巧
当你的解法出现问题时,可以:
- 打印dp数组的中间状态,检查是否符合预期
- 对小规模测试用例手动计算,与程序输出对比
- 特别注意循环的起始和终止条件
5.3 算法选择建议
根据问题规模选择合适解法:
- n ≤ 10^3:O(n^2)的DP解法足够
- n ≤ 10^5:必须使用O(nlogn)的优化解法
- 需要重建LIS序列时,DP解法更方便
6. 与其他动态规划问题的关联
最长上升子序列与许多经典DP问题有密切联系:
- 最长公共子序列(LCS)
- 编辑距离
- 背包问题
理解LIS的状态设计和转移思路,有助于掌握这些更复杂的问题。特别是,LIS可以看作是序列上的"决策过程",每个位置决定是否将当前元素纳入子序列,这与背包问题的物品选择非常相似。
