经典算法-最长公共子序列LCS
题目描述:
给定两个字符串 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 。
题目来源:leetcode1143最长公共子序列
解题思路:
此题目要求的最长公共子序列不一定连续,所以不能使用暴力求解或者KMP。对于如此复杂的算法,要先想到切割子问题,对两个完整串的子串一步步对比,分析出它子问题的解,最终得到结果。
以text1 = " aab", text2 = "acb"为例,我们令函数LCS(“aab”,“acb”)为最终结果。我们从后面分析,两个串的最后一个字符都为’b’,显然匹配成功,这时两个串的最长公共子序列至少为1,至于大于还是等于1那就得看剩余子串了,所以有 LCS(“aab”,“acb”) = 1+LCS(“aa”,“ac”) 这是匹配成功的情况。接下来解决LCS(“aa”,“ac”),显然最后一个字符为’a’和’c’没有匹配成功,这时要分两种情况:
①:让"a"和"ac"匹配,即求LCS(“a”,“ac”)。
②:让"aa"和”a“匹配,即求LCS(“aa”,“a”)。
然后取两种情况的最大值。(注意:当有一个串为空时其结果为0,因为空串和任何串的最长公共子序列都为0,不用解释)
原理很简单,递归树如下:
从起始的两个完整串慢慢分解到空串,在逐层返回当前的最长公共子序列长度,通过对子问题的叠加得出总体解。
下面对两个复杂字符串进行分析,text1=“AGGTAB”,text2=“GXTXAYB”,首先列出dp数组如下图:
" " | A | G | G | T | A | B | |
---|---|---|---|---|---|---|---|
" " | |||||||
G | |||||||
X | # | ||||||
T | |||||||
X | |||||||
A | |||||||
Y | |||||||
B | ! |
dp数组的解释:!所在位置表示 LCS(“AGGTAB”,“GXTXAYB”) 的结果,#所在位置表示 LCS(“AGG”,“GX”) 的结果。
首先对dp数组进行初始化,由上面分析可知,空数组与任何串的最长公共子序列都为0,即 LCS("" , 任意)=LCS(任意 ,"") = 0。
初始化后的dp数组:
" " | A | G | G | T | A | B | |
---|---|---|---|---|---|---|---|
" " | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
G | 0 | ||||||
X | 0 | ||||||
T | 0 | ||||||
X | 0 | ||||||
A | 0 | ||||||
Y | 0 | ||||||
B | 0 |
我们由开始得出的结论,假如匹配成功则结果等于1+两个串皆除去最后一个字符,反之取两个串分别除去最后一个与另一个对比的结果。
接下来从LCS(“G”,“A”)分析,显然匹配不成功,则取LCS("",A)和LCS(“G”,"")的最大值,结果都为0,则LCS(“G”,“A”)为0,dp数组的dp[1][1]=0。到了"AG"与"G"对比时,结果匹配成功,则LCS(“AG”,“G”) = 1 + LCS(“A”,""),对于dp数组意义为dp[1][2] = 1 + dp[0][1],第二行结果如下。
" " | A | G | G | T | A | B | |
---|---|---|---|---|---|---|---|
" " | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
G | 0 | 0 | 1 | 1 | 1 | 1 | 1 |
X | 0 | ||||||
T | 0 | ||||||
X | 0 | ||||||
A | 0 | ||||||
Y | 0 | ||||||
B | 0 |
不难发现动态转移方程为
d
p
[
i
]
[
j
]
=
{
0
,
i=0 或 j=0
d
p
[
i
−
1
]
[
j
−
1
]
+
1
,
text[i] = text[j]
m
a
x
(
d
p
[
i
−
1
]
[
j
]
,
d
p
[
i
]
[
j
−
1
]
)
,
text[i]
≠
text[j]
dp[i][j]= \begin{cases} 0,& \text{i=0 或 j=0}\\ dp[i-1][j-1]+1,& \text{text[i] = text[j]}\\ max(dp[i-1][j],dp[i][j-1]),&\text{text[i]$\neq$text[j]} \end{cases}
dp[i][j]=⎩⎪⎨⎪⎧0,dp[i−1][j−1]+1,max(dp[i−1][j],dp[i][j−1]),i=0 或 j=0text[i] = text[j]text[i]=text[j]
最终分析的到完整dp数组为
" " | A | G | G | T | A | B | |
---|---|---|---|---|---|---|---|
" " | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
G | 0 | 0 | 1 | 1 | 1 | 1 | 1 |
X | 0 | 0 | 1 | 1 | 1 | 1 | 1 |
T | 0 | 0 | 1 | 1 | 2 | 2 | 2 |
X | 0 | 0 | 1 | 1 | 2 | 2 | 2 |
A | 0 | 1 | 1 | 1 | 2 | 3 | 3 |
Y | 0 | 1 | 1 | 1 | 2 | 3 | 3 |
B | 0 | 1 | 1 | 1 | 2 | 3 | 4 |
LCS(“AGGTAB”,“GXTXAYB”) = dp[7][6] = 4。答案正确,最长公共子序列为GTAB。
算法代码如下:
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int x_len = text1.size(), y_len = text2.size();
vector<vector<int>> dp(x_len + 1, vector<int>(y_len + 1));
for (int i = 1; i <= x_len; ++i) {
for (int j = 1; j <= y_len; ++j) {
if (text1[i - 1] == text2[j - 1]) //最后一位相同则1+两个长度都-1的dp值
dp[i][j] = 1 + dp[i - 1][j - 1];
else
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
return dp[x_len][y_len];
}
};
算法时间复杂度为O(mn),空间复杂度为O(mn)。
该算法可以优化空间为O(n),有兴趣的可以自行修改。