跳到正文
第 1143 题中等

最长公共子序列

Longest Common Subsequence

求两个字符串的最长公共子序列长度。子序列不要求连续,只要字符相对顺序一致即可。

在力扣中练习

题目描述

给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

  • 例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。

两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

 

示例 1:

输入:text1 = "abcde", text2 = "ace" 
输出:3  
解释:最长公共子序列是 "ace" ,它的长度为 3 。

示例 2:

输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc" ,它的长度为 3 。

示例 3:

输入:text1 = "abc", text2 = "def"
输出:0
解释:两个字符串没有公共子序列,返回 0 。

 

提示:

  • 1 <= text1.length, text2.length <= 1000
  • text1 和 text2 仅由小写英文字符组成。

解题思路

主解法

二维 DP · 末尾字符分情况

比较两个前缀的最后一个字符,相同就 +1,不同就扔掉一边

子序列可以跳过字符,但保留原有相对顺序。用 dp[i][j] 表示两个前缀的最长公共子序列长度,就能把“最后一个字符要不要保留”变成局部判断。

自拟主例 text1="abca",text2="baca" 含重复字符和错位匹配,答案为 3,例如 bca 或 aca;它们不必在两个原串中都连续。

分步图解

(1,1):'a' 不同于 'b',取上方和左方较大值,写入 0。

公共子序列 · 前缀长度表写入答案

dp[i][j]:text1 前 i 个字符与 text2 前 j 个字符的 LCS 长度

i \ jεbacaεabca0初dp[0][0]:00dp[0][1]:0,选中的来源0dp[0][2]:00dp[0][3]:00dp[0][4]:00dp[1][0]:0,选中的来源0≠dp[1][1]:0,当前目标·dp[1][2]:尚未计算·dp[1][3]:尚未计算·dp[1][4]:尚未计算0dp[2][0]:0·dp[2][1]:尚未计算·dp[2][2]:尚未计算·dp[2][3]:尚未计算·dp[2][4]:尚未计算0dp[3][0]:0·dp[3][1]:尚未计算·dp[3][2]:尚未计算·dp[3][3]:尚未计算·dp[3][4]:尚未计算0dp[4][0]:0·dp[4][1]:尚未计算·dp[4][2]:尚未计算·dp[4][3]:尚未计算·dp[4][4]:尚未计算dp[0][1] → dp[1][1]:上方dp[1][0] → dp[1][1]:左方
dp[1][1]=max(dp[0][1], dp[1][0])
=max(0, 0)=0

并列最优 0

未计算初值
1 / 3
相同时读左上并加一,不同时比较上方和左方。

末尾字符相同时,把它们一起接在左上前缀的答案之后,得到 dp[i-1][j-1]+1。不同时,至少要丢掉一边的末尾,比较上方和左方的结果,不能直接读左上。

分步图解

(4,1):'a' 不同于 'b',取上方和左方较大值,写入 1。

公共子序列 · 前缀长度表写入答案

dp[i][j]:text1 前 i 个字符与 text2 前 j 个字符的 LCS 长度

i \ jεbacaεabca0初dp[0][0]:00dp[0][1]:00dp[0][2]:00dp[0][3]:00dp[0][4]:00dp[1][0]:00dp[1][1]:01dp[1][2]:11dp[1][3]:11dp[1][4]:10dp[2][0]:01dp[2][1]:11dp[2][2]:11dp[2][3]:11dp[2][4]:10dp[3][0]:01dp[3][1]:1,选中的来源1dp[3][2]:12dp[3][3]:22dp[3][4]:20dp[4][0]:0,候选来源1≠dp[4][1]:1,当前目标·dp[4][2]:尚未计算·dp[4][3]:尚未计算·dp[4][4]:尚未计算dp[3][1] → dp[4][1]:上方dp[4][0] → dp[4][1]:左方
dp[4][1]=max(dp[3][1], dp[4][0])
=max(1, 0)=1

选中 dp[3][1] 上方

未计算初值
1 / 4
按行填表时,当前格依赖的三个方向都已经确定。

空串对应第 0 行和第 0 列,初值均为 0。按行从左到右填表时,三个依赖方向都已经计算完。最后返回的是两个完整前缀对应的格子,而不是表中随便一个局部匹配长度。

时间 O(m × n)空间 O(m × n)

易错点

  • 下标错位:dp 的第 i 行对应 text1[i-1],写成 text1[i] 会越界或错配。
  • dp 只开 m×n 不留哨兵行列,i-1 / j-1 就得处处特判,很容易漏。
  • 把子序列当成子串:子串要求连续,失配时 dp[i][j] 要直接清零,而不是从上方 / 左方取 max。
对照解法

滚动数组 · 一维 DP

每一行只依赖上一行,把二维表压成一行

正确的空间优化版,只保留上一行信息;更省空间,但不能直接保留完整的状态表。

观察转移方程:dp[i][j] 只用到 dp[i-1][j-1]、dp[i-1][j] 和 dp[i][j-1],也就是上一行和本行左边。所以不需要保存整个表。

把二维压成一维后,dp[j] 在覆盖前是 dp[i-1][j](上方),覆盖后是 dp[i][j](本行)。左边格 dp[j-1] 已经更新过,可以直接用。

麻烦的是左上角 dp[i-1][j-1]:它会被覆盖掉。用一个变量 prev 记住这一轮开始前的 dp[j]:

temp = dp[j]          // 旧值 = dp[i-1][j],下一列的左上角
dp[j] = prev + 1      // 匹配时:prev 正是 dp[i-1][j-1]
prev = temp           // 交给 j+1 用

空间从 O(m×n) 降到 O(n),代价是要多想清楚 prev 的时序,写错了不会报错,只会悄悄算错。

  1. 1dp 数组长度 n+1,初始全 0。
  2. 2每行开始前 prev = 0,代表 dp[i-1][0]。
  3. 3temp 先存下覆盖前的 dp[j](上方值)。
  4. 4相同则 dp[j] = prev + 1,否则 dp[j] = max(dp[j], dp[j-1])。
  5. 5prev = temp,为下一列提供左上角。
时间 O(m × n)空间 O(n)

易错点

  • 忘记更新 prev,所有匹配都用了错误的左上角,答案偏小。
  • 把 prev = 0 提到 i 循环外面,下一行会沿用上一行末尾的残值。
  • 先更新 dp[j] 再取 temp,上方值已经被覆盖,等于把 dp[i-1][j] 弄丢了。

代码与演示

主解法配有图解;对照解法用于比较思路、复杂度或实现方式。

演示输入text1 = "abca", text2 = "baca"

空串与任何前缀的 LCS 为 0,先填第 0 行和第 0 列。

公共子序列 · 前缀长度表建立初值

dp[i][j]:text1 前 i 个字符与 text2 前 j 个字符的 LCS 长度

i \ jεbacaεabca0初dp[0][0]:0,当前目标0dp[0][1]:00dp[0][2]:00dp[0][3]:00dp[0][4]:00dp[1][0]:0·dp[1][1]:尚未计算·dp[1][2]:尚未计算·dp[1][3]:尚未计算·dp[1][4]:尚未计算0dp[2][0]:0·dp[2][1]:尚未计算·dp[2][2]:尚未计算·dp[2][3]:尚未计算·dp[2][4]:尚未计算0dp[3][0]:0·dp[3][1]:尚未计算·dp[3][2]:尚未计算·dp[3][3]:尚未计算·dp[3][4]:尚未计算0dp[4][0]:0·dp[4][1]:尚未计算·dp[4][2]:尚未计算·dp[4][3]:尚未计算·dp[4][4]:尚未计算
dp[0][0]=0
未计算初值
1 / 8
查看 C++ 代码

延伸阅读

  • 子序列不要求连续,所以不匹配时可以「丢掉一边」;子串要求连续,转移就得在失配时清零(如最长重复子数组 718)。
  • 编辑距离(72)是同一张二维表:匹配时不动,不匹配时在增、删、改三种操作里取最小。
  • 两个字符串的 DP 几乎都是这个形状:i、j 分别指向两个前缀,讨论 `text1[i-1]` 与 `text2[j-1]` 的关系。