news 2026/10/27 11:51:15

动态规划经典:最长子序列三题精解与对比

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
动态规划经典:最长子序列三题精解与对比

1. 动态规划经典问题精讲:最长子序列三连击

今天咱们来啃动态规划里最经典的骨头——最长子序列问题。这三个题目看似相似,实则各有玄机,很多同学在面试和刷题时容易混淆。我结合自己刷了300+道动态规划题的经验,给大家拆解这三个问题的核心差异和解题套路。

先说说为什么这三个题目值得放在一起对比学习。它们都涉及"最长子序列"这个概念,但约束条件不同:300题是最长递增子序列(LIS),不要求连续;674题要求连续递增;718题则是两个数组的最长公共子数组(要求连续)。理解这些细微差别,对掌握动态规划的状态定义至关重要。

2. LeetCode 300. 最长递增子序列(LIS)

2.1 问题本质与暴力解法

给定一个无序整数数组nums,找到其中最长严格递增子序列的长度。子序列不要求连续,但相对顺序不能改变。例如[10,9,2,5,3,7,101,18]的最长递增子序列是[2,3,7,101],长度为4。

最直观的暴力解法是枚举所有子序列,检查是否递增。一个长度为n的数组有2^n个子序列,时间复杂度O(2^n),显然不可行。

2.2 动态规划解法

定义dp[i]为以nums[i]结尾的最长递增子序列长度。状态转移方程:

dp[i] = max(dp[j]) + 1, 其中0 ≤ j < i且nums[j] < nums[i]

初始化时每个dp[i]至少为1(自身构成子序列)。最终结果是dp数组中的最大值。

def lengthOfLIS(nums): dp = [1] * len(nums) 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) return max(dp) if dp else 0

时间复杂度O(n^2),空间复杂度O(n)。

2.3 贪心+二分优化

更优的解法是用贪心思想维护一个递增序列tails,其中tails[i]表示长度为i+1的子序列的最小末尾元素。遍历时用二分查找确定当前元素的位置:

def lengthOfLIS(nums): tails = [] for num in nums: left, right = 0, len(tails) while left < right: mid = (left + right) // 2 if tails[mid] < num: left = mid + 1 else: right = mid if left == len(tails): tails.append(num) else: tails[left] = num return len(tails)

时间复杂度降为O(n log n),适合处理大规模数据。

关键点:理解tails数组的含义——维护的是"相同长度下末尾最小"的子序列,这样后续元素更容易接上。

3. LeetCode 674. 最长连续递增序列

3.1 与300题的差异

本题要求子序列必须是连续的,这实际上简化为寻找最长的递增连续子数组。例如[1,3,5,4,7]的最长连续递增序列是[1,3,5],长度为3。

3.2 滑动窗口解法

由于要求连续,可以用滑动窗口维护当前递增区间:

def findLengthOfLCIS(nums): if not nums: return 0 max_len = curr_len = 1 for i in range(1, len(nums)): if nums[i] > nums[i-1]: curr_len += 1 max_len = max(max_len, curr_len) else: curr_len = 1 return max_len

时间复杂度O(n),空间复杂度O(1)。

3.3 动态规划解法

虽然滑动窗口更优,但为了保持解法统一,也可以用DP:

def findLengthOfLCIS(nums): if not nums: return 0 dp = [1] * len(nums) for i in range(1, len(nums)): if nums[i] > nums[i-1]: dp[i] = dp[i-1] + 1 return max(dp)

注意:连续条件下,dp[i]只与dp[i-1]有关,状态转移更简单。

4. LeetCode 718. 最长重复子数组

4.1 二维动态规划解法

给两个整数数组A和B,返回两个数组中公共的、长度最长的子数组的长度。这里的子数组要求连续。

定义dp[i][j]为以A[i-1]和B[j-1]结尾的最长公共子数组长度(i,j从1开始):

def findLength(A, B): m, n = len(A), len(B) dp = [[0]*(n+1) for _ in range(m+1)] max_len = 0 for i in range(1, m+1): for j in range(1, n+1): if A[i-1] == B[j-1]: dp[i][j] = dp[i-1][j-1] + 1 max_len = max(max_len, dp[i][j]) return max_len

时间复杂度O(mn),空间复杂度O(mn)。

4.2 空间优化解法

观察到dp[i][j]只依赖dp[i-1][j-1],可以优化空间:

def findLength(A, B): m, n = len(A), len(B) max_len = 0 dp = [0] * (n + 1) for i in range(1, m + 1): for j in range(n, 0, -1): # 反向遍历避免覆盖 if A[i-1] == B[j-1]: dp[j] = dp[j-1] + 1 max_len = max(max_len, dp[j]) else: dp[j] = 0 # 不相等要重置 return max_len

空间复杂度降为O(n)。

4.3 滑动窗口解法

还有一种巧妙的滑动窗口对齐方法,通过偏移量对齐两个数组:

def findLength(A, B): def maxLength(addA, addB, length): max_len = k = 0 for i in range(length): if A[addA + i] == B[addB + i]: k += 1 max_len = max(max_len, k) else: k = 0 return max_len m, n = len(A), len(B) result = 0 for i in range(m): length = min(n, m - i) result = max(result, maxLength(i, 0, length)) for j in range(n): length = min(m, n - j) result = max(result, maxLength(0, j, length)) return result

时间复杂度O((m+n)*min(m,n)),空间复杂度O(1)。

5. 三题对比与常见误区

5.1 状态定义差异总结

题目状态定义连续性要求转移方程特点
300dp[i]: 以nums[i]结尾的LIS长度不连续需要遍历之前所有元素
674dp[i]: 以nums[i]结尾的连续递增序列长度必须连续只与前一个元素有关
718dp[i][j]: 以A[i-1]和B[j-1]结尾的最长公共子数组必须连续需要两个维度,且要求元素相等

5.2 常见错误与调试技巧

  1. 边界条件处理:

    • 空数组输入
    • 单元素数组
    • 所有元素相同的情况
  2. 初始化问题:

    • 300题每个dp[i]至少为1
    • 718题dp数组要多开一行一列
  3. 状态转移混淆:

    • 674题误用300题的转移方式
    • 718题忘记在不相等时重置dp值

调试建议:先用小例子手动计算dp表格,再与程序输出对比。

6. 实战应用与扩展

6.1 面试变种问题

  1. 输出具体的LIS而不仅是长度(300题)
  2. 最长递增子序列的个数(300题变种)
  3. 允许有一个"波动"的最长递增序列(如[1,3,2,4])
  4. 三维的最长公共子数组(718题升维)

6.2 工程应用场景

  1. 版本差异比对(如git diff的算法基础)
  2. DNA序列比对(生物信息学)
  3. 文本相似度计算(自然语言处理)
  4. 股票趋势分析(金融领域)

6.3 刷题训练建议

  1. 建议刷题顺序:

    • 先674(最简单)
    • 再300(理解不连续的情况)
    • 最后718(二维DP)
  2. 同类题目推荐:

      1. 最长公共子序列(不要求连续)
      1. 不相交的线(718题的变种)
      1. 不同的子序列(更难的状态转移)
  3. 我个人的刷题心得是:每天坚持做2-3道DP题,连续一个月后会有质的飞跃。初期可以先把经典题型的模板背下来,再慢慢理解内在逻辑。这三道题建议手写DP表格至少5次,直到能闭眼画出状态转移过程。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/27 11:51:11

Codex CLI 实战:从需求到前端组件秒级生成

最近我把自己的前端开发工作流重新捋了一遍&#xff0c;Codex CLI 是给我最大惊喜的一个工具。尤其是组件开发这件事——以前写轮播图、级联选择器、表单联动这类东西&#xff0c;总要在结构框架上花掉不少时间&#xff0c;现在从一句需求描述到一组能直接跑的 React/Vue 组件&…

作者头像 李华
网站建设 2026/10/27 11:49:40

风电故障诊断中的时空特征融合技术与数据集构建

1. 项目概述这个数据集是我在风电行业工作十年间积累的宝贵资源&#xff0c;专门用于解决风电机组故障诊断中的时空特征融合难题。不同于常规SCADA数据&#xff0c;我们通过特殊的数据采集和处理流程&#xff0c;将时间序列的运行参数与空间分布的机组状态信息进行了深度融合。…

作者头像 李华
网站建设 2026/10/27 11:49:38

AI训练师如何让大模型更“接地气”?从提示词、评估集到Agent编排

闪学it联合且曼推出的AI训练师训练营刚开营&#xff0c;我朋友圈就被刷屏了。后台私信里问得最多的三个问题是&#xff1a;AI训练师到底是什么岗位&#xff1f;现在学能不能赶上窗口期&#xff1f;训练营到底教什么干货&#xff1f;作为在IT培训行业带过几年学员、自己也深度参…

作者头像 李华
网站建设 2026/10/27 11:49:36

openGauss数据库实验全流程:从建库建表到事务备份与JDBC对账

简介&#xff1a;《数据库系统原理实验》报告面向计算机科学与技术、软件工程专业学生及数据库初学者&#xff0c;围绕 openGauss 环境下的课程实验整理而成&#xff0c;可用于巩固数据库理论、完成实验报告与复习 SQL 操作。压缩包共 1 个 docx 文件&#xff0c;约 3.23MB&…

作者头像 李华
网站建设 2026/10/27 11:49:08

DataEase日志查看与问题排查全指南

1. DataEase日志查看命令全解析 作为DataEase平台的深度用户&#xff0c;我经常需要查看系统日志来排查问题。经过半年多的实战积累&#xff0c;这套日志查看命令组合拳已经成为我的日常工具箱必备技能。今天就把这些干货毫无保留地分享给大家&#xff0c;包含从基础查询到高级…

作者头像 李华
网站建设 2026/10/27 11:47:43

基于K-Means与PCA的评论情感分析及ECharts可视化实现

这个题目我第一次看到的时候&#xff0c;第一反应是"名字太长了吧"。但是如果你把这串字拆开&#xff0c;其实特别清晰&#xff1a;前面半句是算法内核——K-Means聚类加主成分分析&#xff08;PCA&#xff09;做消费者评论情感挖掘&#xff0c;后面半句是系统呈现—…

作者头像 李华