1. 最长递增子序列问题概述
最长递增子序列(Longest Increasing Subsequence, LIS)是动态规划领域的经典问题,也是算法面试中的高频考点。给定一个整数数组nums,我们需要找到其中最长的严格递增子序列的长度,并统计具有该长度的子序列的个数。
这个问题看似简单,但蕴含着动态规划思想的精髓。在实际应用中,LIS问题可以用于DNA序列比对、股票趋势分析、任务调度优化等多个领域。例如,在金融分析中,我们可以用LIS来识别一支股票价格的最长上涨周期;在生物信息学中,LIS算法可以帮助比较不同物种基因序列的相似性。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 基础解法:动态规划思路解析
2.1 状态定义与转移方程
对于基础的LIS问题(仅求长度),我们通常定义dp[i]表示以nums[i]结尾的最长递增子序列的长度。状态转移方程为:
code复制dp[i] = max(dp[j] + 1) for all j < i and nums[j] < nums[i]
这个方程的含义是:对于每个元素nums[i],我们检查它之前的所有元素nums[j],如果nums[j]小于nums[i],那么nums[i]可以接在nums[j]后面形成更长的子序列。
2.2 统计子序列数量的扩展
为了统计最长递增子序列的个数,我们需要在原有dp数组的基础上,增加一个count数组。count[i]表示以nums[i]结尾的最长递增子序列的个数。
当更新dp[i]时:
- 如果发现dp[j] + 1 > dp[i],说明找到了更长的子序列,此时需要重置count[i]为count[j]
- 如果dp[j] + 1 == dp[i],说明找到了相同长度的不同子序列,此时count[i] += count[j]
2.3 初始化与边界条件
初始时,每个元素本身就是一个长度为1的子序列,因此:
- dp数组初始化为1(每个元素至少可以形成长度为1的子序列)
- count数组初始化为1(每个元素至少有一种方式形成长度为1的子序列)
3. 完整算法实现与代码解析
3.1 Python实现代码
python复制def findNumberOfLIS(nums):
if not nums:
return 0
n = len(nums)
dp = [1] * n # dp[i]: 以nums[i]结尾的LIS长度
count = [1] * n # count[i]: 以nums[i]结尾的LIS数量
for i in range(n):
for j in range(i):
if nums[j] < nums[i]:
if dp[j] + 1 > dp[i]:
dp[i] = dp[j] + 1
count[i] = count[j]
elif dp[j] + 1 == dp[i]:
count[i] += count[j]
max_len = max(dp)
return sum(c for l, c in zip(dp, count) if l == max_len)
3.2 关键代码段解析
-
双重循环结构:外层循环遍历每个元素,内层循环检查当前元素之前的所有元素,这是典型的动态规划实现方式。
-
状态更新逻辑:
- 当发现更长的子序列时(dp[j]+1 > dp[i]),重置count[i]为count[j]
- 当发现相同长度的子序列时(dp[j]+1 == dp[i]),累加count[i] += count[j]
-
结果统计:最后找出所有dp值等于最大长度的count值之和。
3.3 复杂度分析
- 时间复杂度:O(n²),因为有两层嵌套循环
- 空间复杂度:O(n),需要两个长度为n的数组dp和count
4. 算法优化与进阶思路
4.1 贪心+二分查找优化
对于仅求LIS长度的问题,存在O(nlogn)的优化解法,使用贪心算法结合二分查找。基本思路是维护一个tails数组,其中tails[i]表示长度为i+1的所有递增子序列的最小末尾元素。
python复制def lengthOfLIS(nums):
tails = []
for num in nums:
i = bisect.bisect_left(tails, num)
if i == len(tails):
tails.append(num)
else:
tails[i] = num
return len(tails)
4.2 统计数量的优化挑战
遗憾的是,这种优化方法难以直接扩展到统计数量的情况。因为贪心算法只保留了最小末尾元素,丢失了其他可能形成相同长度子序列的信息。因此,当需要统计数量时,O(n²)的动态规划方法仍然是主流选择。
4.3 实际应用中的权衡
在实际应用中,我们需要根据具体需求选择算法:
- 如果只关心长度且数据规模大,优先选择O(nlogn)的贪心算法
- 如果需要统计数量或数据规模不大,使用O(n²)的动态规划方法
5. 常见错误与调试技巧
5.1 初始化错误
常见错误是忘记初始化dp和count数组为1。如果初始化为0,会导致整个算法逻辑错误。
注意:每个元素至少可以形成长度为1的子序列,因此初始值必须为1。
5.2 状态转移条件混淆
容易混淆的条件是nums[j] < nums[i]而不是nums[j] <= nums[i],题目要求的是严格递增。在实际面试中,很多候选人会在这里犯错。
5.3 结果统计遗漏
最后统计结果时,需要遍历所有count值,找出对应dp值等于最大长度的进行累加。容易犯的错误是直接返回count[-1],这在大多数情况下是不正确的。
5.4 测试用例设计
有效的测试用例应该包括:
- 空数组
- 单元素数组
- 完全递增数组
- 完全递减数组
- 有重复元素的数组
- 多个最长子序列的情况
例如:
- 输入:[1,3,5,4,7]
- 最长子序列长度:4
- 数量:2([1,3,4,7]和[1,3,5,7])
- 输入:[2,2,2,2,2]
- 最长子序列长度:1
- 数量:5(每个元素单独形成的子序列)
6. 实际应用案例与变种问题
6.1 股票交易策略分析
在量化交易中,我们可以使用LIS算法识别股票价格的最长上涨周期。统计最长上涨周期的数量可以帮助分析市场趋势的强弱。如果存在多个相同长度的上涨周期,可能表明该股票价格在这个区间有较强的支撑。
6.2 基因序列比对
在生物信息学中,比较两个基因序列的相似性时,可以将其转化为LIS问题。统计最长匹配子序列的数量有助于评估序列间的进化关系。
6.3 变种问题举例
- 最长非递减子序列:将条件从严格递增改为非严格递增(允许相等)
- 二维LIS问题:元素是二维点,需要同时满足x和y的递增关系
- 带权LIS:每个元素有权重,求权重和最大的递增子序列
- 多个序列的公共LIS:求多个序列共有的最长递增子序列
7. 个人解题心得与建议
在实际解决这个问题时,我总结了以下几点经验:
-
从简单情况入手:先解决仅求长度的问题,再考虑统计数量,分步骤思考可以降低难度。
-
画图辅助理解:对于数组[1,3,5,4,7],可以画出每个位置的dp和count值变化过程,直观理解算法逻辑。
-
注意边界条件:特别是空数组和所有元素相同的情况,这些边界条件往往是面试官考察的重点。
-
优化意识:虽然O(n²)的解法已经足够好,但了解O(nlogn)的优化思路可以展现更深入的算法理解。
-
代码简洁性:Python的zip和列表推导式可以让代码更简洁,如最后的结果统计部分。
