子序列要求元素顺序一致就可以了,而字串必须是连续的。如ABCBDAB与BDCABA两个字符串,最长公共子序列有BCBA、BDAB和BCAB, 而最长公共字串只有AB和BD<连续>。当然这里的求解只求一个,但通常是这样直接说求最长公共子串,子序列,准确的应该是之一。
最长公共子序列
法一:穷举法
检查字符串x所有字序列,共有2^m个,检查它是否在y字符串中出现,每个需要O(n),时间复杂度为指数级的。
法二:动态规划(DP)
将两个字符串x[1…m]和y[1…n]放在x轴和y轴方向上便得到一个二维数组c[i,j]来记录x[1…i]和y[1…j]最长公共子序列个数。
当x[i]==y[j]的时候 c[i,j] = c[i-1,j-1]+1;当不相等的时候,c[i,j] = max{c[i-1, j], c[i,j-1]}.
采用自底向上的思想,这样时间复杂度就等于lcs独立子问题的个数O(mn),不然需要重复计算子问题,时间复杂度仍然为指数级的。
代码如下:
// 最长公共子序列(不连续) 需要有个标记数组用于回溯
void lcs_sequences(const char* str1, const char* str2, int len1, int len2)
{
int **c = new int*[len1+1];
int **b = new int*[len1+1];
int i, j;
for(i = 0; i < len1+1; i++)
{
c[i] = new int[len2+1];
b[i] = new int[len2+1];
}
for(i = 0; i <= len1; i ++)
for(j = 0; j <= len2; j++)
c[i][j] = 0;
for(i = 1; i <= len1; i++)
{
for(j = 1; j <= len2; j++)
{
if(str1[i-1] == str2[j-1])
{
c[i][j] = c[i-1][j-1] +1;
b[i][j] = 0; // 来自左上
}
else{
if(c[i-1][j] > c[i][j-1]){
c[i][j] = c[i-1][j];
b[i][j] = 1; // 来自上方
}
else{
c[i][j] = c[i][j-1]; // 来自左方
b[i][j] = 2; // 来自上方
}
}
}
}
cout << "最长公共子序列长度: " << c[len1][len2] << endl;
// 回溯求解路径
i = len1;
j = len2;
char *x = new char[c[len1][len2]];
int k = 0;
/*while(i > 0 && j > 0){
if(b[i][j] == 0) // 来自左上
{
x[k++] = str1[i-1];
//cout << str1[i-1];
i--;
j--;
}
else if(b[i][j] == 1) i--;
else j--;
}*/
// 不使用标记数组进行回溯 直接使用str1和str2及c[i][j]得出结果
while(i > 0 && j > 0)
{
if(str1[i-1] == str2[j-1])
{
x[k++] = str1[i-1];
i--;
j--;
}
else if(c[i][j] == c[i][j-1]) j--;
else i--;
}
cout << "the lcs_opt is: " ;
for(i = c[len1][len2]-1; i >= 0 ; i--)
{
cout << x[i];
}
cout << endl;
for(i= 0; i < len1; i++)
delete[] c[i];
delete []c;
delete []x;
}上面注释的代码,我们用标记数组来跟踪来源,当然也可以不适用标记数字,直接使用c[i,j]与str1、str2来判断,这里就可以节省O(mn)的空间,但是只是在空间复杂性的常数因子上的改进。
如果只要求公共字串的长度而不求字串是什么,则空间复杂度可以降低到O(min{m,n}).这里用到了二维数组,但是行数固定为2.
代码如下:
// 最长公共子序列优化空间,用两行的二维数组, 但此时只能得到最长公共子序列的长度, 无法得到路径
void swap(int **c, int len2)
{
for(int i = 0; i < len2; i++)
{
int temp = c[0][i];
c[0][i] = c[1][i];
c[1][i] = temp;
}
}
void lcs_sequences_opt(const char* str1, const char* str2, int len1, int len2)
{
int *c[2];
int i,j;
for(i = 0; i < 2; i++)
c[i] = new int[len2];
for(j = 0; j < len2; j++)
c[0][j] = 0;
for(i = 0; i < len1; i++)
{
for(j = 0; j < len2; j++)
{
if(str1[i] == str2[j])
{
if(j == 0) c[1][j] = 1;
else c[1][j] = c[0][j-1] + 1;
}
else
{
if(j == 0) c[1][j] = c[0][j];
else c[1][j] = c[0][j] > c[1][j-1] ? c[0][j]: c[1][j-1];
}
}
swap(c, len2); // 不用交换 直接将c[1]赋值给c[0]就可以了
}
cout << "最长公共子序列长度为: " << c[0][len2-1] << endl;
for(i = 0; i < 2; i++)
delete[] c[i];
}
最长公共子串
解法就是用一个矩阵来记录两个字符串中所有位置的两个字符之间的匹配情况,若是匹配则为1,否则为0。然后求出对角线最长的1序列,其对应的位置就是最长匹配子串的位置.
优化:当字符匹配的时候,我们并不是简单的给相应元素赋上1,而是赋上其左上角元素的值加一。我们用两个标记变量来标记矩阵中值最大的元素的位置,在矩阵生成的过程中来判断当前生成的元素的值是不是最大的,据此来改变标记变量的值,那么到矩阵完成的时候,最长匹配子串的位置和长度就已经出来了。
即:
当x[i]==y[j]的时候 c[i,j] = c[i-1,j-1]+1;当不相等的时候,c[i,j] = 0.
代码如下:
// 求最长公共子串
void lcs_string(const char*str1, const char *str2, int len1, int len2)
{
int **c = new int*[len1];
int i, j;
int maxC = 0; // 最大值
int position = 0; // 位置
for(i = 0; i < len1; i++)
{
c[i] = new int[len2];
for(j = 0; j < len2; j++)
{
if(str1[i] == str2[j])
{
if(i == 0 || j == 0)
{
c[i][j] = 1;
}
else{
c[i][j] = c[i-1][j-1] + 1;
}
}
else{
c[i][j] = 0;
}
if(c[i][j] > maxC)
{
maxC = c[i][j];
position = j;
}
}
}
cout << "最大公共子串长度为: " << maxC << endl;
cout << "the lcs is: " ;
for(i = position - maxC+1; i <= position; i++)
{
cout << str2[i];
}
cout << endl;
for(i = 0; i < len1; i++)
delete[] c[i];
delete []c;
}此时时间复杂度和空间复杂度都为O(mn)。当然也可以对代码的空间复杂度进行优化到O(min{m,n}),这里只需要用一维数组就可以了,但是从后到前进行遍历,这样c[j]前面的才是上一次的结果,否则如DBB与AB就会出错。
代码如下:
// 求最长公共子串 优化空间复杂度 用一维数组就可以搞定
void lcs_string_opt(const char*str1, const char *str2, int len1, int len2)
{
int *c = new int[len2];
int i, j;
int maxC = 0; // 最大值
int position = 0; // 位置
memset(c, 0, sizeof(int)*len2);
for(i = 0; i < len1; i++)
{
for(j = len2-1; j >= 0; j--) // 从后到前进行遍历 这样c[j]前面的才是上一次的结果 否则如DBB与AB就会出错
{
if(str1[i] == str2[j])
{
if(j == 0) c[j] = 1;
else c[j] = c[j-1]+1; // 唯一的区别
}
else c[j] = 0;
if(c[j] > maxC)
{
maxC = c[j];
position = j;
}
}
}
cout << "最大公共子串长度为: " << maxC << endl;
cout << "the lcs_opt is: " ;
for(i = position - maxC+1; i <= position; i++) // 输出最长公共字串
{
cout << str2[i];
}
cout << endl;
delete []c;
}动态规划
动态规划具有两大特性,我们通过最长公共子序列来作为例子。
1:最优子结构
意思是问题的最优解包含了子问题的最优解。
如lcs中求x[1…i]和y[1…j]的最长公共子序列,当x[i]==y[j]时,可以转换为求x[1…i-1]和y[1…j-1]的最长公共子序列。当x[i]不等于y[j],需要计算出x[1…i]和y[1…j-1]及x[1…i-1]和y[1…j]的最长公共子序列。而这两个子问题都包含一个公共子问题,即计算x[1…i-1]和y[1…j-1]的最长公共子序列。
2:重叠子问题
我们也看到子问题都包含一个公共子问题,即会出现重叠问题。
意思就是指一个递归问题包含少数独立的子问题被反复计算了多次。lcs问题中就包含了m*n个独立的子问题。
参考文献
1:http://blog.csdn.net/steven30832/article/details/8260189
2:http://blog.csdn.net/imzoer/article/details/8031478
原文地址:http://blog.csdn.net/lu597203933/article/details/43560641