class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
}
};
最长公共子序列的问题常用于解决字符串的相似度,是一个非常实用的算法
首先,让我们先来看一下子串、子序列还有公共子序列的概念
那么现在,我们再通俗的总结一下最长公共子序列(LCS):就是A和B的公共子序列中长度最长的(包含元素最多的)
仍然用序列1,3,5,4,2,6,8,7和序列1,4,8,6,7,5为例,它们的最长公共子序列有1,4,8,7和1,4,6,7两种,但最长公共子序列的长度是4。由此可见,最长公共子序列(LCS)也不一定唯一。
(1)准备一个表
(2)对于X = “ABCDAB”, Y="BDCABA",各自取出最短的序列,也就是空字符串和空字符串比较。此时两个空字符串的公共区域的长度为0
(3)然后我们X不动,继续让空字符串出阵,Y让“B”出阵,很显然,它们的公共区域的长度为0. Y换成其他字符, D啊,C啊, 或者, 它们的连续组合DC、 DDC, 情况没有变, 依然为0. 因此第一行都为0. 然后我们Y不动,Y只出空字任串,那么与上面的分析一样,都为0,第一列都是0.
LCS问题与背包问题有些不一样,背包问题还可以设置-1行,而最长公共子序列因为有空子序列的出现,一开始就把左边与上边固定死了。
(4)然后我们把问题放大些,这次双方都出一个字符,显然只有两者都相同时,才存在不为空字符串的公共子序列,长度此时为1。
如下,A为"X", Y为"BDCA"的子序列的任意一个
继续往右填空,该怎么填?显然,LCS不能大于X的长度,Y的从A字符串开始的子序列与B的A序列相比,怎么也能等于1。(只要一个序列只有一个字符,那么另一个序列无论多长,它们的最长公共子序列长度最多只能为1)
(5)如果X只从派出前面个字符A,B,亦即是“”,“A”, “B”, "AB"这四种组合,
{"",A,B,AB} VS {"",B,D,BD}
,显然,继续填1.{"",A,B,AB} VS {"",B,D,BD, C, BC, DC, BDC}
,显然,继续填1.{"",A,B,AB} VS {"",B,D,BD, C, BC, DC, BDC, A, BA, DA, BDA, CA, BCA, DCA, BDCA}
,显然,继续填1.(7)Y将所有字符派上去,X依然是2个字符,经仔细观察,还是填2.
X再多派一个C,ABC的子序列集合比AB的子序列集合大一些,那么它与Y的B子序列集合大一些,就算不大,就不能比原来的小。显然新增的C不能成为战力,不是两者的公共字符,因此值应该等于AB的子序列集合。
这时我们可以确定,如果两个字符串要比较的字符不一样,那么要填的格子是与其左边或上边有关,那边大就取那个。
如果比较的字符一样呢,稍安毋躁,刚好X的C要与Y的C进行比较,即ABC的子序列集合{“”,A,B,C,AB,BC,ABC}与BDC的子序列集合{“”,B,D,C,BD,DC,BDC}比较,得到公共子串有“”,B,C 。这时还是与之前的结论一样,当字符相等时,它对应的格子值等于左边/右边/左上角的值 + 1,并且左边,上边,左上边总是相等的。这些奥秘需要更严格的数学知识来论证。
假设有两个数组,A和B。A[i]为A的第i个元素,A(i)为由A的第一个元素到第i个元素所组成的前缀。m(i, j)为A(i)和B(j)的最长公共子序列长度。
由于算法本身的递推性质,其实只要证明,对于某个i和j:
第一个式子很好证明,即当A[i] = B[j]时。可以用反证,假设m(i, j) > m(i-1, j-1) + 1 (m(i, j)不可能小于m(i-1, j-1) + 1,原因很明显),那么可以推出m(i-1, j-1)不是最长的这一矛盾结果。
第二个有些trick。当A[i] != B[j]时,还是反证,假设m(i, j) > max( m(i-1, j), m(i, j-1) )。
由反证假设,可得m(i, j) > m(i-1, j)。这个可以推出A[i]一定在m(i, j)对应的LCS序列中(反证可得)。而由于A[i] != B[j],故B[j]一定不在m(i, j)对应的LCS序列中。所以可推出m(i, j) = m(i, j-1)。这就推出了与反正假设矛盾的结果。
得证。
简单来说,填写table[i][j]的规律就是:相等左上角+1,不等取上或者右最大值,如果上、左一样大,那么优先取左
比如下面,想要求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)
问题:对于 s1[i] 和 s2[j] 不相等的情况,至少有一个字符不在 lcs 中,会不会两个字符都不在呢?比如下面这种情况:
所以代码是不是应该考虑这种情况,改成这样:
if str1[i - 1] == str2[j - 1]:
# ...
else:
dp[i][j] = max(dp[i-1][j],
dp[i][j-1],
dp[i-1][j-1])
可以是可以,但是多此一举,因为 dp[i-1][j-1] 永远是三者中最小的,max 根本不可能取到它。
定义子问题table[i][j]为字符串A的第一个字符到第i个字符和字符串B的第一个字符到第j个字符串的最长公共子序列。 如A为“app”,B为“apple”,table[ 2 ][ 3 ]表示 “ap” 和 “app” 的最长公共字串。
class LCS {
public:
int findLCS(string A, int n, string B, int m) {
// write code here
int table[n + 1][m + 1];
for(int i = 0;i <= n;++i)table[i][0] = 0;
for(int i = 0;i <= m;++i)table[0][i] = 0;
for(int i = 0;i < n;++i){
for(int j = 0;j < m;++j){
if(A[i] == B[j])
table[i + 1][j + 1] = table[i][j] + 1;
else {
table[i + 1][j + 1] = max(table[i][j + 1],table[i + 1][j]);
}
}
}
return table[n][m];
}
};
我们完成填表后,只能求出最长公共子序列的长度,但是无法得知它的具体构成。我们可以从填表的反向角度来寻找子序列。
我们子序列保存在名为 s的数组中,从表格中反向搜索,找到目标字符后,每次都把目标字符插入到数组最前面。
根据前面提供的填表口诀,我们可以反向得出寻找子序列的口诀: 如果T[i][j]来自左上角加一,则是子序列,否则向左或上回退。如果上左一样大,优先取左。
s = ['d']
s = ['a','d']
最终箭头指向0,搜索结束。
s = ['a','b','a','d']
伪代码:
if(input1[i] == input2[j]){
s.insertToIndexZero(input1[i]); //插入到数组最前面
i--;
j--;
}else{
//向左或向上回退
if(T[i-1][j]>T[i][j-1]){
//向上回退
i--;
}else{
//向左回退
j--;
}
}
现在我们只关心 s t r 1 [ 0... i ] , s t r 2 [ 0... j ] str1[0...i],str2[0...j] str1[0...i],str2[0...j],对于它们的最长公共子序列长度是多少。
int process(std::string str1, std::string str2, int i, int j);
因此,主函数应该这样调用:
return process(str1, str2, str1.size() - 1, str2.size() - 1);
那么,这个递归函数应该怎么实现呢?我们来分析当前位置的可能性(可能做出的决策)。
(1) 如果i == 0 && j == 0
:然后str1[1] == str2[j] ? 1 : 0
(2)如果i == 0
时,也就是str1只有一个字符的时候
str1[i] == str2[j]
时,那么返回1str1[i] != str2[j]
时,此时有没有str2[j]都一样,所以我们丢弃strs[j],那么返回process(str1, str2, i, j - 1);
(3) 如果j == 0
时,也就是str2只有一个字符的时候
str1[i] == str2[j]
时,那么返回1str1[i] != str2[j]
时,那么返回process(str1, str2, i-1, j );
(4) 如果i != 0 && j != 0
:
实现:
class Solution {
int process(string &text1, string &text2, int i, int j){
if(i == 0 && j == 0){
return text1[i] == text2[j] ? 1 : 0;
}else if(i == 0){
if(text1[i] == text2[j] ){
return 1;
}else{
return process(text1, text2, i, j - 1);
}
}else if(j == 0){
if(text1[i] == text2[j]){
return 1;
}else{
return process(text1, text2, i - 1, j);
}
}else{
int p1 = process(text1, text1, i - 1, j);
int p2 = process(text1, text2, i, j - 1);
int p3 = text1[i] == text2[j] ? 1 + process(text1, text2, i - 1, j - 1) : 0;
return std::max(p1, std::max(p2, p3));
}
}
public:
int longestCommonSubsequence(string text1, string text2) {
if(text1.empty() || text2.empty()){
return 0;
}
return process(text1, text2, (int)text1.size() - 1, (int)text2.size() - 1);
}
};
(1)分析可变参数
int process(std::string str1, std::string str2, int i, int j);
因为有两个变化维度,所以需要一个二维数组
int dp[N][M];
(3)分析basecase和依赖
(4)最终
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
if(text1.empty() || text2.empty()){
return 0;
}
int N = text1.size(), M = text2.size();
int dp[N][M];
dp[0][0] = text1[0] == text2[0] ? 1 : 0;
for (int i = 1; i < N; ++i) {
dp[i][0] = text1[i] == text2[0] ? 1 : dp[i - 1][0];
}
for (int j = 1; j < M; ++j) {
dp[0][j] = text1[0] == text2[j] ? 1 : dp[0][j - 1];
}
for (int i = 1; i < N; ++i) {
for (int j = 1; j < M; ++j) {
int p1 = dp[i - 1 ][j];
int p2 = dp[i][j - 1];
int p3 = text1[i] == text2[j] ? 1 + dp[i - 1][j - 1] : 0;
dp[i][j] = std::max(p3, std::max(p1, p2));
}
}
return dp[N - 1][M - 1];
}
};
javascript 最长公共子序列
详解动态规划最长公共子序列–JavaScript实现