最长公共子序列 LCS (Longest Common Subsequence)被广泛用于生产中,如 git 中对比 commit 串等,本文简述一下动态规划解决 LCS 算法的问题。

子序列定义 Link to heading

对于某个序列 $l = [a_1, a_2, …, a_n]$ ,称序列 $l’ = [a_{i_1}, a_{i_2}, …, a_{i_k}]$ ,其中 $i_1 < i_2 < … < i_k$ 为序列 $l$ 的子序列。注意这个子序列指需要保证有序,不需要保证连续

衍生描述:不相交的线

LCS 问题算法 Link to heading

使用动态规划解决。定义 $dp[i][j]$ 描述序列 1 前 i 个元素和序列 j 前 j 个元素的最长公共子序列。

  • 当 $i = 0$ 或 $j = 0$ 时,易得 $dp[i][j] = 0$;

  • 当 $i > 0$ 且 $j > 0$ 时:

    • 若 $l_1[i] = l_2[j]$,此时必然有 $dp[i][j] = dp[i-1][j-1] + 1$。

    • 若 $l_1[i] \neq l_2[j]$,此时有 $dp[i][j] = max(dp[i-1][j], dp[i][j-1])$。

代码实现 Link to heading

1def lcs(l1, l2):
2    dp = [[0] * (len(l2) + 1) for _ in range(len(l1) + 1)]
3    for i in range(1, len(l1) + 1):
4        for j in range(1, len(l2) + 1):
5            if l1[i - 1] == l2[j - 1]:
6                dp[i][j] = dp[i - 1][j - 1] + 1
7            else:
8                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
9    return dp[-1][-1]
 1function lcs(l1: any[], l2: any[]) {
 2  const dp = [...Array(l1.length+1)].map(() => Array(l2.length+1).fill(0))
 3  for (let i = 0; i < l1.length; i++) {
 4    for (let j = 0; j < l2.length; j++) {
 5      if (l1[i] === l2[j]) {
 6        dp[i+1][j+1] = dp[i][j] + 1
 7      } else {
 8        dp[i+1][j+1] = Math.max(dp[i][j+1], dp[i+1][j])
 9      }
10    }
11  }
12
13  return dp.at(-1).at(-1)
14}