一、算法概述
- 问题定义:最长公共子序列(Longest Common Subsequence,简称LCS)问题是指,给定两个序列,找到它们最长的公共子序列的长度。子序列不要求元素在原序列中连续,但需保持相对顺序。例如,序列
[1, 3, 4, 5, 6, 7, 7, 8]和[3, 5, 7, 4, 8, 6, 7, 8, 2]的最长公共子序列是[3, 5, 7, 8],长度为 4。 - 应用场景:该算法广泛应用于文本比较(如 diff 工具)、生物信息学(DNA 序列比对)、语音识别等领域,用于衡量两个序列的相似程度。
二、算法思路
- 动态规划思想:通过将原问题分解为多个子问题,利用子问题的解构建最终答案,避免重复计算。
- 状态定义:定义二维数组
dp[i][j]表示序列a的前i个元素和序列b的前j个元素的最长公共子序列长度。 - 状态转移方程:
- 若
a[i]等于b[j],则dp[i][j] = dp[i-1][j-1] + 1,即公共子序列长度增加 1。 - 若
a[i]不等于b[j],则dp[i][j] = max(dp[i-1][j], dp[i][j-1]),取去掉a[i]或去掉b[j]后的子问题的最优解。
- 若
- 路径记录:使用二维数组
path[i][j]记录状态转移的方向,方便后续回溯得到具体的最长公共子序列。其中,path[i][j] = 0表示a[i] == b[j]时的转移,path[i][j] = 1表示从dp[i-1][j]转移,path[i][j] = 2表示从dp[i][j-1]转移。
三、伪代码实现
算法:最长公共子序列
输入:序列 a[1..n],序列 b[1..m]
输出:a 和 b 的最长公共子序列长度
function getLCS(n, a[], m, b[]):
创建二维数组 dp[0..n][0..m] 用于存储最长公共子序列长度
创建二维数组 path[0..n][0..m] 用于记录状态转移路径
// 初始化边界条件:空序列与任何序列的 LCS 长度为 0
for i from 0 to n:
dp[i][0] = 0
for j from 0 to m:
dp[0][j] = 0
for i from 1 to n:
for j from 1 to m:
if a[i] == b[j]:
dp[i][j] = dp[i-1][j-1] + 1
path[i][j] = 0
else if dp[i-1][j] > dp[i][j-1]:
dp[i][j] = dp[i-1][j]
path[i][j] = 1
else:
dp[i][j] = dp[i][j-1]
path[i][j] = 2
return dp[n][m]
// 主程序
输入 n, m
输入序列 a[1..n]
输入序列 b[1..m]
输出 getLCS(n, a, m, b)
四、算法解释
- 初始化阶段:
- 将
dp[i][0]和dp[0][j]初始化为 0,表示空序列与任何序列的最长公共子序列长度为 0。
- 将
- 状态转移过程:
- 双重循环遍历序列
a和b的每个元素。 - 若
a[i] == b[j],说明找到了一个公共元素,dp[i][j]由dp[i-1][j-1]转移而来,并将path[i][j]标记为 0。 - 若
a[i] != b[j],比较dp[i-1][j]和dp[i][j-1],取较大值作为dp[i][j],并根据取值情况标记path[i][j]为 1 或 2。
- 双重循环遍历序列
- 回溯获取子序列(未在伪代码中体现,但为完整逻辑):
- 从
dp[n][m]开始,根据path数组的记录回溯,若path[i][j] == 0,则a[i](或b[j])是公共子序列的元素,向左上方移动;若path[i][j] == 1,向上移动;若path[i][j] == 2,向左移动。
- 从
- 示例演示:
- 设
a = [1, 3, 4, 5],b = [3, 5, 4]。 - 初始化后,
dp[0][j]和dp[i][0]均为 0。 - 遍历过程中,当
i = 2, j = 1时,a[2] = 3等于b[1] = 3,则dp[2][1] = dp[1][0] + 1 = 1,path[2][1] = 0;当i = 2, j = 2时,a[2] != b[2],dp[2][1] = 1大于dp[1][2] = 0,则dp[2][2] = dp[2][1] = 1,path[2][2] = 1。最终计算得到dp[4][3]即为最长公共子序列长度。
- 设
五、复杂度分析
- 时间复杂度:算法使用两层嵌套循环遍历两个序列,因此时间复杂度为
O
(
n
×
m
)
O(n \times m)
O(n×m),其中
n和m分别是两个序列的长度。 - 空间复杂度:使用了两个二维数组
dp和path,空间复杂度为 O ( n × m ) O(n \times m) O(n×m)。在某些情况下,由于计算dp[i][j]只依赖于dp[i-1][j]、dp[i][j-1]和dp[i-1][j-1],可以将空间优化至 O ( m i n ( n , m ) ) O(min(n, m)) O(min(n,m))。 - 与暴力解法对比:暴力解法需枚举两个序列的所有子序列并比较,时间复杂度为指数级,而动态规划算法通过子问题复用大幅降低了时间复杂度。
本文为作者(英雄哪里出来)在抖音的独家课程《英雄C++入门到精通》、《英雄C语言入门到精通》、《英雄Python入门到精通》三个课程的配套文字讲解,如需了解算法视频课程,请移步 作者本人 的抖音直播间。
29万+

被折叠的 条评论
为什么被折叠?



