文章目录
1. 题目
给定两个字符串 text1
和 text2
,返回这两个字符串的最长公共子序列的长度。如果不存在公共子序列,返回 0 0 0 。
一个字符串的子序列是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,"ace"
是 "abcde"
的子序列,但 "aec"
不是 "abcde"
的子序列。两个字符串的公共子序列是这两个字符串所共同拥有的子序列。
1.1 示例
- 示例 1 1 1:
- 输入:
text1 = "abcde"
,text2 = "ace"
- 输出: 3 3 3
- 解释: 最长公共子序列是
"ace"
,它的长度为 3 3 3 。
-
示例 2 2 2:
-
输入:
text1 = "abc"
,text2 = "abc"
-
输出: 3 3 3
-
解释: 最长公共子序列是
"abc"
,它的长度为 3 3 3 。 -
示例 3 3 3:
-
输入:
text1 = "abc"
,text2 = "def"
-
输出: 0 0 0
-
解释: 两个字符串没有公共子序列,返回 0 0 0 。
1.2 说明
- 来源: 力扣(LeetCode)
- 链接: https://leetcode-cn.com/problems/longest-common-subsequence
1.3 提示
1 <= text1.length, text2.length <= 1000
text1
和text2
仅由小写英文字符组成。
1.4 进阶
当 text1
和 text2
存在最长公共子序列时,你可以进一步输出该最长公共子序列么?
2. 解法一(动态规划)
2.1 分析
2.1.1 定义状态
dp[i][j]
:表示text1
长度为i
的前缀子序列和text2
长度为j
的前缀子序列二者最长公共子序列的长度。
这里我们引出了前缀子序列的概念,为便于后续讨论,下面给出前缀子序列的严格定义:
给定任意的序列 X = ( x 1 , x 2 , ⋯ , x m ) X=(x_1,x_2,\cdots,x_m) X=(x1,x2,⋯,xm) ,对于 i = 0 , 1 , ⋯ , m i=0,1,\cdots,m i=0,1,⋯,m ,则称 X i = ( x 1 , x 2 , ⋯ , x i ) X_i=(x_1,x_2,\cdots,x_i) Xi=(x1,x2,⋯,xi) 为 X X X 的第 i i i 个前缀子序列,显然 X 0 X_0 X0 是一个空序列。
2.1.2 初始化状态
显然当 i
或 j
为 0 0 0 时 dp[i][j]
均为 0 0 0 。
2.1.3 状态转移
实际上,如果分别给定两个序列 X = ( x 1 , x 2 , ⋯ , x m ) X=(x_1,x_2,\cdots,x_m) X=(x1,x2,⋯,xm) 和 Y = ( y 1 , y 2 , ⋯ , y n ) Y=(y_1,y_2,\cdots,y_n) Y=(y1,y2,⋯,yn) ,且已知 Z = ( z 1 , z 2 , ⋯ , z k ) Z=(z_1,z_2,\cdots,z_k) Z=(z1,z2,⋯,zk) 为 X X X 和 Y Y Y 的任意一个最长公共子序列,则有如下性质:
- 如果 x m = y n x_m = y_n xm=yn ,则必然有 z k = x m = y n z_k = x_m = y_n zk=xm=yn 且 Z k − 1 Z_{k - 1} Zk−1 是 X m − 1 X_{m - 1} Xm−1 和 Y n − 1 Y_{n - 1} Yn−1 的一个最长公共子序列;
- 如果 x m ≠ y n x_m \ne y_n xm=yn ,则由 z k ≠ x m z_k \ne x_m zk=xm 可推定 Z Z Z 是 X m − 1 X_{m - 1} Xm−1 和 Y Y Y 的一个最长公共子序列;
- 如果 x m ≠ y n x_m \ne y_n xm=yn ,则由 z k ≠ y n z_k \ne y_n zk=yn 可推定 Z Z Z 是 X X X 和 Y n − 1 Y_{n - 1} Yn−1 的一个最长公共子序列。
下面给出对于上述性质的证明:
- 首先证明 z k = x m z_k = x_m zk=xm ,这里采用反证法,即先假设 z k ≠ x m z_k \ne x_m zk=xm ,则可以将 x m = y n x_m = y_n xm=yn 追加至 Z Z Z 的最后,此时得到了序列 X X X 和 Y Y Y 的一个长度为 k + 1 k + 1 k+1 的公共子序列,这和 Z Z Z (长度为 k k k)是 X X X 和 Y Y Y 的最长公共子序列这一条件相矛盾,因此必有 z k = x m = y n z_k = x_m = y_n zk=xm=yn ;此外还需要证明 Z k − 1 Z_{k - 1} Zk−1 是 X m − 1 X_{m - 1} Xm−1 和 Y n − 1 Y_{n - 1} Yn−1 的一个最长公共子序列,对此先假设 X m − 1 X_{m - 1} Xm−1 和 Y n − 1 Y_{n - 1} Yn−1 有一个长度大于 k − 1 k - 1 k−1 的公共子序列 W W W ,此时将 x m = y n x_m = y_n xm=yn 追加至 W W W 末尾就得到了 X X X 和 Y Y Y 的一个长度大于 k k k 的最长公共子序列,同样和条件矛盾,证毕;
- 如果 z k ≠ x m z_k \ne x_m zk=xm ,则显然 Z Z Z 是 X m − 1 X_{m - 1} Xm−1 和 Y Y Y 的一个公共子序列,假设 X m − 1 X_{m - 1} Xm−1 和 Y Y Y 有长度大于 k k k 的公共子序列 W W W ,则 W W W 必然也是 X m X_{m} Xm 和 Y Y Y 的公共子序列,这和 Z Z Z 为 X X X 和 Y Y Y 的最长公共子序列相矛盾,证毕;
- 证明方式同上。
实际上,根据上述性质可知:
- 当 x m = y n x_m = y_n xm=yn 时,通过在 X m − 1 X_{m - 1} Xm−1 和 Y n − 1 Y_{n - 1} Yn−1 的最长公共子序列后面追加 x m = y n x_m = y_n xm=yn ,即得到 X X X 和 Y Y Y 的一个最长公共子序列;
- 当 x m ≠ y n x_m \ne y_n xm=yn 时, X m − 1 X_{m - 1} Xm−1 和 Y Y Y 的最长公共子序列同 X X X 和 Y n − 1 Y_{n - 1} Yn−1 的最长公共子序列中更长的那个即为 X X X 和 Y Y Y 的一个最长公共子序列。
据此,我们可以写出下列状态转移方程:
d p [ i , j ] = { 0 i = 0 or j = 0 d p [ i − 1 , j − 1 ] i , j > 0 and x i = y j max ( d p [ i , j − 1 ] , d p [ i − 1 , j ] ) i , j > 0 and x i ≠ y j dp[i, j]= \begin{cases} 0& {i = 0} \text { or } {j = 0} \\ dp[i - 1, j - 1]& {i \text{, } j \gt 0} \text{ and } {x_i = y_j} \\ \text{max} (dp[i, j - 1], dp[i - 1, j])& {i \text{, } j \gt 0} \text{ and } {x_i \ne y_j} \end{cases} dp[i,j]=⎩⎪⎨⎪⎧0dp[i−1,j−1]max(dp[i,j−1],dp[i−1,j])i=0 or j=0i, j>0 and xi=yji, j>0 and xi=yj
当 i > 0
且 j > 0
时:
- 如果
text1[i - 1]
和text2[j - 1]
相同,则dp[i][j] = dp[i - 1][j - 1] + 1
; - 如果
text1[i - 1]
和text2[j - 1]
不相同,则dp[i][j]
取dp[i - 1][j]
和dp[i ][j - 1]
二者较大值。
2.1.4 返回结果
最终 dp[m][n]
即为所求的结果,其中 m m m 和 n n n 分别为 text1
和 text2
的长度。
2.2 解答
需要注意的是,下面代码并不是采用自顶向下的解法而是采用自下而上的解法:
class Solution:
def longest_common_subsequence(self, text1: str, text2: str) -> int:
m, n = len(text1), len(text2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
if text1[i - 1] == text2[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1
else:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
return dp[m][n]
def main():
text1 = "abcde"
text2 = "ace"
sln = Solution()
print(sln.longest_common_subsequence(text1, text2)) # 3
if __name__ == '__main__':
main()
- 执行用时: 288 ms , 在所有 Python3 提交中击败了 90.36% 的用户;
- 内存消耗: 22.6 MB , 在所有 Python3 提交中击败了 50.58% 的用户。
2.3 复杂度
- 时间复杂度: O ( m n ) O(mn) O(mn),其中 m m m 和 n n n 分别是字符串
text1
和text2
的长度。二维数组dp
有 m + 1 m+1 m+1 行和 n + 1 n+1 n+1 列,需要对dp
中的每个元素进行计算;- 空间复杂度: O ( m n ) O(mn) O(mn),其中 m m m 和 n n n 分别是字符串
text1
和text2
的长度。创建了 m + 1 m+1 m+1 行 n + 1 n+1 n+1 列的二维数组dp
。
3. 重建 LCS
3.1 递归重建
为了计算出两个给定序列的一个最长公共子序列,我们还需要一个辅助的二维数组 paths
,其中 paths[i][j]
保存了在计算 dp[i][j]
时所使用的 dp
元素。例如:
paths[i][j] = 'LEFT_UPWARD'
表示计算dp[i][j]
时由dp[i - 1][j - 1]
这一状态转移过来;paths[i][j] = 'UPWARD'
表示计算dp[i][j]
时由dp[i - 1][j]
这一状态转移过来;paths[i][j] = 'LEFTWARD'
表示计算dp[i][j]
时由dp[i][j - 1]
这一状态转移过来。
下表是使用 dp
和 paths
计算两个序列 "ABCBDAB"
和 "BDCABA"
最长公共子序列时 dp[i][j]
和 paths[i][j]
的取值情况,其中值得注意的是:
dp[7][6]
就是上述两个给定序列的最长公共子序列的长度;- 对任意
i > 0
且j > 0
,元素dp[i][j]
的值只在 x i = y j x_i = y_j xi=yj 时取决于dp[i - 1][j - 1]
,或者在 x i ≠ y j x_i \ne y_j xi=yj 时取决于dp[i - 1][j]
或dp[i][j - 1]
,而这三个元素都在计算dp[i][j]
之前就已经算出来了; - 当需要计算两个序列的一个最长公共子序列时,只要沿着从
paths[7][6]
开始的箭头回溯即可,具体地,每次遇到指向左上方的箭头,即paths[i][j] == 'LEFT_UPWARD'
时,这意味着 x i = y j x_i = y_j xi=yj 就是最长公共子序列的一个元素。
下面即是根据上述分析,实现了计算一个最长子序列:
from typing import List, Tuple
class Solution:
def __init__(self):
self._lcs = []
def _reconstruct_lcs(self, paths: List[List[str]], text1: str, i: int, j: int):
if i == 0 or j == 0:
return
if paths[i][j] == 'LEFT_UPWARD':
self._reconstruct_lcs(paths, text1, i - 1, j - 1)
self._lcs.append(text1[i - 1])
elif paths[i][j] == 'UPWARD':
self._reconstruct_lcs(paths, text1, i - 1, j)
else:
self._reconstruct_lcs(paths, text1, i, j - 1)
def calc_lcs(self, text1: str, text2: str) -> Tuple[int, List[List[str]]]:
m, n = len(text1), len(text2)
paths = [[''] * (n + 1) for _ in range(m + 1)]
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
if text1[i - 1] == text2[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1
paths[i][j] = 'LEFT_UPWARD'
elif dp[i - 1][j] >= dp[i][j - 1]:
dp[i][j] = dp[i - 1][j]
paths[i][j] = 'UPWARD'
else:
dp[i][j] = dp[i][j - 1]
paths[i][j] = 'LEFTWARD'
self._reconstruct_lcs(paths, text1, m, n)
return dp[m][n], self._lcs
def main():
text1 = "ABCBDAB"
text2 = "BDCABA"
sln = Solution()
length, lcs = sln.calc_lcs(text1, text2)
print(length, lcs) # 4 ['B', 'C', 'B', 'A']
if __name__ == '__main__':
main()
对于上述实现,需要特别注意的一点是,由于其中变量 i
和 j
都是代表前缀子序列的长度,因此当 x i = y j x_i = y_j xi=yj 时, x i x_i xi 对应 text1[i - 1]
。
3.2 迭代重建
递归式重建虽然容易理解,但递归终归有两个缺陷,即资源开销大以及有最大递归深度限制,下面给出了一种迭代重建的方式,这种方式不仅不存在递归式解法的问题,而且将空间复杂度降低了常数倍,因为我们不在需要创建一个单独的二维列表 paths
:
from typing import List
class Solution:
def _calc_dp(self, text1: str, text2: str) -> List[List[int]]:
m, n = len(text1), len(text2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
if text1[i - 1] == text2[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1
else:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
return dp
def iterative_calc_lcs(self, text1: str, text2: str) -> str:
dp = self._calc_dp(text1, text2)
sln = []
i, j = len(text1), len(text2)
while i > 0 and j > 0:
if text1[i - 1] == text2[j - 1]:
sln.append(text1[i - 1])
i -= 1
j -= 1
elif dp[i - 1][j] >= dp[i][j - 1]:
i -= 1
else:
j -= 1
return ''.join(reversed(sln))
def main():
text1 = "ABCBDAB"
text2 = "BDCABA"
sln = Solution()
print(sln.iterative_calc_lcs(text1, text2)) # BCBA
if __name__ == '__main__':
main()
最后关于对 sln = ['A', 'B', 'C', 'B']
分别使用 reversed
类创建对象以及使用列表的 reverse()
方法产生的效果如下所示:
sln = ['A', 'B', 'C', 'B']
print(sln)
['A', 'B', 'C', 'B']
for char in reversed(sln):
print(char)
B
C
B
A
sln.reverse()
print(sln)
['B', 'C', 'B', 'A']
即:
- 使用
reversed
类会创建一个迭代器对象而不会改变列表本身; - 调用列表的
reverse()
方法将会对列表做原地的逆序操作,会改变列表本身。