最长公共子序列
概述
最长公共子序列(LCS)问题是给定两个序列 和 ,求它们的最长公共子序列的长度和具体序列。该问题是字符串算法的基础问题,广泛应用于文件差异比较(diff工具)和生物信息学(DNA序列比对)等领域。
定义
最长公共子序列(Longest Common Subsequence)
输入:两个序列 和 。
输出: 和 的最长公共子序列(LCS)。
设 为 的前缀 和 的前缀 的 LCS 长度,则递推公式为:
核心性质
1. 最优子结构
LCS 的最优子结构体现在以下三种情况:
- 若 ,则 (或 )一定属于某个 LCS,且去掉末尾元素后的 LCS 就是 和 的 LCS
- 若 且 ,则 和 的 LCS 就是 和 的 LCS
- 若 且 ,则 和 的 LCS 就是 和 的 LCS
2. 重叠子问题
递归求解 LCS 时,子问题 和 都会进一步调用 ,导致大量重复计算。例如,计算 时, 会被多次访问。
3. 自底向上算法
LCS-LENGTH(X, Y):
m = X.length
n = Y.length
let b[1..m, 1..n] and c[0..m, 0..n] be new tables
for i = 1 to m:
c[i,0] = 0
for j = 0 to n:
c[0,j] = 0
for i = 1 to m:
for j = 1 to n:
if x_i == y_j:
c[i,j] = c[i-1,j-1] + 1
b[i,j] = "↖" // 左上方向:匹配
else if c[i-1,j] >= c[i,j-1]:
c[i,j] = c[i-1,j]
b[i,j] = "↑" // 上方:取X的前缀
else:
c[i,j] = c[i,j-1]
b[i,j] = "←" // 左方:取Y的前缀
return c and b
4. 构造LCS
通过方向表 回溯构造具体的 LCS:
PRINT-LCS(b, X, i, j):
if i == 0 or j == 0:
return
if b[i,j] == "↖":
PRINT-LCS(b, X, i-1, j-1)
print x_i
else if b[i,j] == "↑":
PRINT-LCS(b, X, i-1, j)
else:
PRINT-LCS(b, X, i, j-1)
回溯过程从 开始,遇到”↖“时输出对应字符,遇到”↑“或”←“时沿对应方向移动,直到到达边界。
5. 空间优化
标准算法的空间复杂度为 。但注意到计算 时只需要第 行和第 行的值,因此可以:
- 使用两行数组滚动计算,空间降至
- 若只需要 LCS 长度(不需要构造具体序列),可以进一步优化
复杂度分析
| 方法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 朴素递归 | ||
| 自底向上(标准) | ||
| 自底向上(空间优化) |
应用场景
- diff 工具:Unix/Linux 的
diff命令基于 LCS 算法比较两个文件的差异 - 版本控制系统:Git 等工具使用 LCS 变体进行代码差异比较
- 生物信息学:DNA/蛋白质序列比对,寻找进化上的保守区域
- 拼写检查:基于编辑距离(与 LCS 密切相关)的纠错建议
章节扩展
- 14.4 最长公共子序列 — LCS 问题的完整求解过程
- 动态规划 — 动态规划完整方法论
- 最优子结构 — 最优子结构性质的详细说明
- 重叠子问题 — 重叠子问题性质的详细说明