最长递增子序列问题笔记
问题背景与建模
最长递增子序列(LIS) 是经典的动态规划问题之一。其核心在于在一个给定的序列中,找到一个最长的子序列,使得这个子序列中的元素是严格递增的。该问题不仅在学术研究中具有重要地位,还在许多实际应用中发挥作用,例如基因序列分析、股票价格预测等。
问题定义:
给定一个长度为 n 的序列 nums,我们需要找到一个最长的子序列,使得这个子序列中的元素是严格递增的。
输入输出:
- 输入:一个数组
nums,表示给定的序列。 - 输出:最长递增子序列的长度。
分析与思考
解决该问题需要综合考虑以下两个因素:
- 递增性:子序列中的元素必须是严格递增的。
- 最长性:我们需要找到最长的满足递增条件的子序列。
由于无法直接穷举所有可能的子序列(组合数为 2^n,当 n 较大时不可行),我们需要更高效的算法。动态规划是解决此类问题的理想工具。
动态规划思路
3.1 状态定义
设 dp[i] 表示以 nums[i] 结尾的最长递增子序列的长度。这里,i 的范围是从 0 到 n-1。
dp[0]表示以nums[0]结尾的最长递增子序列的长度(始终为 1)。dp[n-1]表示以nums[n-1]结尾的最长递增子序列的长度,即我们要求解的目标之一。
3.2 转移方程
对于每个元素 nums[i],我们需要考虑它之前的所有元素 nums[j](其中 j < i):
- 如果
nums[i] > nums[j],则nums[i]可以接在nums[j]后面形成一个递增子序列。 - 因此,状态转移方程为:
[
dp[i] = \max(dp[i], dp[j] + 1) \quad \text{for all } j < i \text{ and } nums[j] < nums[i]
]
3.3 初始化与边界条件
- 初始时,每个元素自身可以构成一个长度为 1 的递增子序列,即
dp[i] = 1对于所有i。
3.4 遍历顺序
- 外层遍历
i,表示当前考虑的元素。 - 内层遍历
j,表示在i之前的元素,检查是否可以形成递增子序列。
4.1 复杂度分析
- 时间复杂度:动态规划的核心是双重循环。外层遍历
n个元素,内层遍历i之前的元素,因此时间复杂度为 (O(n^2))。 - 空间复杂度:我们只使用一个一维数组
dp,因此空间复杂度为 (O(n))。
解题步骤:
-
初始化
dp数组:dp = [1, 1, 1, 1, 1, 1, 1, 1]。 -
考虑第 1 个元素
nums[1] = 9:9不能接在10后面,dp[1]保持为1。
-
考虑第 2 个元素
nums[2] = 2:2不能接在10和9后面,dp[2]保持为1。
-
考虑第 3 个元素
nums[3] = 5:5可以接在2后面,dp[3] = max(dp[3], dp[2] + 1) = 2。
-
考虑第 4 个元素
nums[4] = 3:3可以接在2后面,dp[4] = max(dp[4], dp[2] + 1) = 2。
-
考虑第 5 个元素
nums[5] = 7:7可以接在5和3后面,dp[5] = max(dp[5], dp[3] + 1, dp[4] + 1) = 3。
-
考虑第 6 个元素
nums[6] = 101:101可以接在7后面,dp[6] = max(dp[6], dp[5] + 1) = 4。
-
考虑第 7 个元素
nums[7] = 18:18可以接在7后面,dp[7] = max(dp[7], dp[5] + 1) = 4。
最终结果:dp = [1, 1, 1, 2, 2, 3, 4, 4],最长递增子序列的长度为 4。
总结与启发
最长递增子序列问题虽然是一道经典的算法题,但它的解决思路远不局限于“写出正确代码”。在解决问题的过程中,我们需要:
- 从问题建模开始,明确输入、输出和约束条件。
- 分析解法的核心思路,找出限制因素与优化目标。
- 权衡解法优劣,结合实际需求选择合适的算法。
更重要的是,最长递增子序列的思想广泛应用于现实场景,例如基因序列分析、股票价格预测等。在这些应用中,动态规划的思维和解决方法,能够帮助我们找到更高效、更科学的解决方案。