求两个字符串的最长公共子串和最长公共序列
基本概念
最长公共子串(Longest Common Substring)问题是寻找两个或多个已知字符串最长的子串,且子串却必须是连续的。
最长公共子序列(Longest Common Subsequence)问题的区别在于子序列且子串不是连续的。
问题描述
有两个字符串str和str2,求出两个字符串中最长公共子串长度。
示例 1:
str1 = “acbcbcef”
str2 = “abcbced”
str和str2的最长公共子串为"bcbce"
最长公共子串长度为5
示例 2:
str1 = “acbcbcef”
str2 = “abcbced”
str和str2的最长公共子串为"BDAB”、“BCAB”、“BCBA”。
最长公共子序列的长度4。
最长公共子串
这里以c/c++语言来具体化
- 建立一个二维矩阵,其行列分别对应两个字符串
- 比较二维矩阵中行列相等的点,相等即设置标记点为1
- 之后重复操作,再次遇到行列相等点则,在累计标记点值(累计标记点即为最长公共子串)
代码
/*最长公共子串动态规划表*/
string getLCS(string str1, string str2)
{
vector<vector<int> > record(str1.length(), vector<int>(str2.length()));
int maxLen = 0, maxEnd = 0;/*maxlen累计标记点,maxEnd为最长子串的最后一个元素*/
for(int i=0; i<static_cast<int>(str1.length()); ++i)
{
for (int j = 0; j < static_cast<int>(str2.length()); ++j)
{
if (str1[i] == str2[j])
{
if (i == 0 || j == 0)
{
record[i][j] = 1;
}
else
{
record[i][j] = record[i - 1][j - 1] + 1;
}
}
else
{
record[i][j] = 0;/*i和j为其他数都为0*/
}
}
if (record[i][j] > maxLen)
{
maxLen = record[i][j];
maxEnd = i; //若记录i,则最后获取LCS时是取str1的子串
}
}
return str1.substr(maxEnd - maxLen + 1, maxLen);
}
这个动态数组在计算的达效的同时,还可以保存长度和末位的元素,通过string类中的substr操作来提取出最长公共子串
最长公共序列
理解了上述的二维矩阵概念,这个最长公共序列就好理解了
- 建立一个二维矩阵,其行列分别对应两个字符串
- 比较二维矩阵中行列相等的点,相等即设置标记点为1,遇到不相等的点取左边和上面最大值
- 之后重复操作,再次遇到行列相等点则,在累计标记点值
- 二维矩阵的末位点即为所求最长公共序列的长度
注意:这里的动态规划表为table[str1.length()+1][str2.length()+1]
解决了最长公共序列的长度,那么怎么进行输出子序列呢?
我们需要在动态规划表上进行回溯,具体见下面操作
-
如果格子table[i][j]对应的str1[i-1] == str2[j-1],则把这个字符放入 LCS 中,并在该点斜对角线的上一个点中继续进行判断;
-
如果格子table[i][j]对应的 str1[i-1] ≠ str2[j-1],则比较左边和上面的值,跳入值较大的格子继续进行判断;
-
如果格子table[i][j]对应的 str1[i-1] ≠ str2[j-1],左上两值相等,跳入值左边和上面的格子继续进行判断;
-
可以进行递归操作直到 i 或 j 小于等于零为止,倒序输出 LCS 。
代码
/*动态规划最长公共序列,并输出子序列*/
#include <iostream>
#include <string>
#include <vector>
#include <set>
using namespace std;
string str1 = "ABCBDAB";
string str2 = "BDCABA";
vector<vector<int>> table; // 动态规划表
set<string> setOfLCS; // set保存所有的LCS
int max(int a, int b)
{
return (a>b)? a:b;
}
/* 对字符串进行逆序操作 */
string Reverse(string str)
{
int low = 0;
int high = str.length() - 1;
while (low < high)
{
char temp = str[low];
str[low] = str[high];
str[high] = temp;
++low;
--high;
}
return str;
}
/* 同最长公共子串相同,返回最长序列长度*/
int lcs(int m, int n)
{
// 表的大小为(m+1)*(n+1)
table = vector<vector<int>>(m+1,vector<int>(n+1));
for(int i=0; i<m+1; ++i)
{
for(int j=0; j<n+1; ++j)
{
// 第一行和第一列置0
if (i == 0 || j == 0)
table[i][j] = 0;
else if(str1[i-1] == str2[j-1])
table[i][j] = table[i-1][j-1] + 1;
else
table[i][j] = max(table[i-1][j], table[i][j-1]);
}
}
return table[m][n];
}
/*求出所有的最长公共子序列,并放入set中 */
void traceBack(int i, int j, string lcs_str)
{
while (i>0 && j>0)
{
if (str1[i-1] == str2[j-1])
{
lcs_str.push_back(str1[i-1]);
--i;
--j;
}
else
{
if (table[i-1][j] > table[i][j-1])
--i;
else if (table[i-1][j] < table[i][j-1])
--j;
else // 相等的情况
{
traceBack(i-1, j, lcs_str);
traceBack(i, j-1, lcs_str);
return;
}
}
}
setOfLCS.insert(Reverse(lcs_str));
}
int main()
{
int m = str1.length();
int n = str2.length();
int length = lcs(m, n);
cout << "The length of LCS is " << length << endl;
string str;
traceBack(m, n, str);
set<string>::iterator beg = setOfLCS.begin();
for( ; beg!=setOfLCS.end(); ++beg)
cout << *beg << endl;
getchar();
return 0;
}
动态规划表相对于传统的递归或者是暴力查找更优,时间复杂度o(mn),空间复杂度o(mn)
如果本文对宝宝打开思路有帮助,可以点个赞哦~
参考
[1]南方以北_求两个字符串的最长公共子串_2018_8_12
[2]神奕_【动态规划】Dynamic Programming_2014_11_27
[3]神奕_【动态规划】输出所有的最长公共子序列_2014_11_29