动态规划2:最长公共子序列问题
引言
在上一篇文章中,我们通过爬楼梯问题初步了解了动态规划的基本思想。今天,我们将深入学习一个更加经典且实用的动态规划问题——最长公共子序列(Longest Common Subsequence,LCS)。
这个问题不仅在算法面试中频繁出现,在实际开发中也有广泛应用,比如:
- 版本控制系统中的文件差异比较
- 生物信息学中的DNA序列比对
- 文本编辑器的智能提示功能
问题定义
什么是子序列?
首先,我们需要明确子序列的概念:
- 子序列:从原序列中按照相对顺序选取若干元素组成的新序列
- 注意:子序列不要求连续,但必须保持相对顺序
例如,对于字符串 "ABCDEF"
:
"ACE"
是一个子序列 ✅"BDF"
是一个子序列 ✅"CAB"
不是子序列 ❌(顺序错误)
最长公共子序列问题
给定两个序列,找到它们的最长公共子序列的长度。
1 | 输入:text1 = "abcde", text2 = "ace" |
解题思路
1. 暴力解法思考
最直观的想法是枚举第一个字符串的所有子序列,然后检查每个子序列是否也是第二个字符串的子序列。但这种方法时间复杂度为 O(2^n × m),对于长字符串来说效率太低。
2. 动态规划思路
让我们从状态定义开始思考:
状态定义:
dp[i][j]
表示text1[0...i-1]
和text2[0...j-1]
的最长公共子序列长度
状态转移:
当我们考虑 text1[i-1]
和 text2[j-1]
时:
如果字符相等:
text1[i-1] == text2[j-1]
1
dp[i][j] = dp[i-1][j-1] + 1
如果字符不相等:
text1[i-1] != text2[j-1]
1
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
边界条件:
dp[0][j] = 0
(空字符串与任何字符串的LCS长度为0)dp[i][0] = 0
(同上)
图解分析
让我们通过一个具体例子来理解这个过程:
text1 = "ace"
, text2 = "aec"
创建二维DP表格:
1 | "" a e c |
填表过程:
初始化边界:第一行和第一列都是0
填充 dp[1][1]:比较 ‘a’ 和 ‘a’
- 相等:
dp[1][1] = dp[0][0] + 1 = 1
- 相等:
填充 dp[1][2]:比较 ‘a’ 和 ‘e’
- 不相等:
dp[1][2] = max(dp[0][2], dp[1][1]) = max(0, 1) = 1
- 不相等:
继续填充…
最终答案:dp[3][3] = 2
,表示LCS长度为2。
代码实现
Java实现
1 | public class Solution { |
Python实现
1 | def longestCommonSubsequence(text1: str, text2: str) -> int: |
空间优化
由于每次计算只需要前一行的信息,我们可以将空间复杂度从 O(m×n) 优化到 O(min(m,n)):
1 | public int longestCommonSubsequence(String text1, String text2) { |
复杂度分析
- 时间复杂度:O(m × n),其中 m 和 n 分别是两个字符串的长度
- 空间复杂度:
- 未优化版本:O(m × n)
- 优化版本:O(min(m, n))
扩展问题
1. 输出具体的LCS
如果需要输出具体的最长公共子序列,我们可以通过回溯DP表格来实现:
1 | public String getLCS(String text1, String text2) { |
2. 相关变形题目
实际应用场景
1. 版本控制系统
Git等版本控制工具使用LCS算法来比较文件差异,生成diff输出。
2. 生物信息学
在DNA序列比对中,LCS帮助找到不同物种间的共同基因片段。
3. 文本相似度检测
通过计算两个文本的LCS长度,可以评估它们的相似程度。
总结
最长公共子序列问题是动态规划的经典应用,它展示了如何:
- 合理定义状态:
dp[i][j]
表示前i和前j个字符的LCS长度 - 找到状态转移方程:根据字符是否相等进行分情况讨论
- 优化空间复杂度:利用滚动数组技巧
掌握LCS问题的解法,不仅能帮助你应对相关的算法面试题,还能为解决更复杂的序列问题打下坚实的基础。
在下一篇文章中,我们将继续探讨动态规划的其他经典问题,如背包问题等,敬请期待!
相关文章推荐:
练习建议:
- 在LeetCode上练习相关的动态规划题目
- 尝试手动画出DP表格来加深理解
- 思考如何将这个算法应用到实际项目中