题目1143:最长公共子序列
题目描述
给定两个字符串text1
和text2
,返回这两个字符串的最长公共子序列的长度。
一个字符串的子序列是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。例如,"ace"是"abcde"的子序列,但"aec"不是"abcde"的子序列。两个字符串的公共子序列
是这两个字符串所共同拥有的子序列。
若这两个字符串没有公共子序列,则返回0。
示例 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 <= 1000
1 <= text2.length <= 1000
输入的字符串只含有小写英文字符。
解答技巧
- 动态规划思路
第一步,一定要明确dp
数组的含义。对于两个字符串的动态规划问题,套路是通用的。比如说对于字符串s1
和s2
,一般来说都要构造一个这样的DP table
:
为了方便理解此表,我们暂时认为索引是从1
开始的,待会的代码中只要稍作调整即可。其中,dp[i][j]
的含义是:对于s1[1..i]
和s2[1..j]
,它们的LCS
长度是dp[i][j]
。
比如上图的例子,d[2][4]
的含义就是:对于"ac"和"babc",它们的LCS
长度是2。我们最终想得到的答案应该是dp[3][6]
。
第二步,定义base case
。
我们专门让索引为0
的行和列表示空串,dp[0][..]
和dp[..][0]
都应该初始化为0
,这就是base case
。
比如说,按照刚才dp
数组的定义,dp[0][3]=0
的含义是:对于字符串""和"bab",其LCS
的长度为0
。因为有一个字符串是空串,它们的最长公共子序列的长度显然应该是0。
第三步,找状态转移方程。
这是动态规划最难的一步,不过好在这种字符串问题的套路都差不多,权且借这道题来聊聊处理这类问题的思路。
状态转移说简单些就是做选择,比如说这个问题,是求s1
和s2
的最长公共子序列,不妨称这个子序列为lcs
。那么对于s1
和s2
中的每个字符,有什么选择?很简单,两种选择,要么在lcs
中,要么不在。
这个在和不在就是选择,关键是,应该如何选择呢?这个需要动点脑筋:如果某个字符应该在lcs
中,那么这个字符肯定同时存在于s1
和s2
中,因为lcs
是最长公共子序列嘛。所以本题的思路是这样:
用两个指针i
和j
从后往前遍历s1
和s2
,如果s1[i]==s2[j]
,那么这个字符一定在lcs
中;否则的话,s1[i]
和s2[j]
这两个字符至少有一个不在lcs
中,需要丢弃一个。先看一下递归解法,比较容易理解:
def longestCommonSubsequence(str1, str2) -> int:
def dp(i, j):
# 空串的base case
if i == -1 or j == -1:
return 0
if str1[i] == str2[j]:
# 这边找到一个lcs的元素,继续往前找
return dp(i - 1, j - 1) + 1
else:
# 谁能让lcs最长,就听谁的
return max(dp(i-1, j), dp(i, j-1))
# i和j初始化为最后一个索引
return dp(len(str1)-1, len(str2)-1)
对于第一种情况,找到一个lcs
中的字符,同时将i
,j
向前移动一位,并给lcs
的长度加一;对于后者,则尝试两种情况,取更大的结果。
其实这段代码就是暴力解法,我们可以通过备忘录或者DP table
来优化时间复杂度,比如通过前文描述的DP table
来解决:
def longestCommonSubsequence(str1, str2) -> int:
m, n = len(str1), len(str2)
# 构建DP table和base case
dp = [[0] * (n + 1) for _ in range(m + 1)]
# 进行状态转移
for i in range(1, m + 1):
for j in range(1, n + 1):
if str1[i - 1] == str2[j - 1]:
# 找到一个lcs中的字符
dp[i][j] = 1 + dp[i-1][j-1]
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
return dp[-1][-1]
第四步,空间压缩
从i
的角度来看, 只与前一行有关,其他行无关。可以根据此进行空间压缩,准备一个dp n+1
的长度,其实的0位置存放一个空字符串
准备几个变量:
last
:表示是当前dp[j]
(dp[i][j]
)左上角的数,相当于dp[i-1][j-1]
,初始化的时候为0
tmp
:表示是当前dp[j]
(dp[i][j]
)正上方的数,相当于dp[i- 1][j]
dp[j-1]
:表示是当前dp[j]
(dp[i][j]
)左边的数,相当于dp[i][j-1]
每一轮结束后,last
的值都向前滚动一个,变成正上方的数,也就是tmp
class Solution:
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
m, n = len(text1), len(text2)
if m==0 or n==0: return 0;
dp = [0]*(n+1)
tmp = 0
for i in range(1,m+1,1):
last =0
for j in range(1,n+1,1):
tmp = dp[j]
if text1[i-1] == text2[j-1]:
dp[j]=last+1
else:
dp[j]=max(tmp, dp[j-1]);
last = tmp;
return dp[n]