最长公共子序列

概述

最长公共子序列(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 密切相关)的纠错建议

章节扩展

参见