您的位置:首页 > 其它

最长公共子序列LCS 与最长公共子串 两个问题的动态规化 解法

2015-12-09 22:51 447 查看
一、最长公共子序列

最长公共子序列的问题常用于解决字符串的相似度。

最长公共子序列全称为Longest Common Sequence,LCS,注意与最长公共子串的区别。序列可以不连续,子串一定是连续的。

下面我们先讨论子序列(不连续)。

对于两个字符串str1, str2,它俩的长度分别记为m, n。

如果使用蛮力法去查找两个字符串的LCS,则即使我们忽略两个子串的比较时间,单单来看任意两个子串的比较次数。str1有2m个子串,str2有2n个子串,单单比较次数就有次2m+n,已经是指数时间了。再加上两个子串的比较时间,完全不可行。但这只是假象。

使用缓存来避免重复子问题的计算。

记C[i, j] = LCS{ str1[0...i], str2[0...j] }.

则可以导出下面递推式。

if(str1[i] == str2[j])

C[i, j] = C[i-1, j-1] + 1;

else

C[i, j] = max( C[i, j-1], C[i-1, j] );

由该递推式即可写出LCS函数

记C[N+1][M+1]中第0行与第0列均为0,1...N行 与str2对应。 1...M列与str1对应。

int C[STR2_LEN + 1][STR1_LEN + 1] = { -1 }; //这里只是将第一个元素初始化为-1,剩余的元素均为0,易误认为这里已经将所有元素都初始化为-1了

void initC()
{
//先全部初始化为-1
memset(C, 0xFF, sizeof(C));
for (int i = 0; i < STR1_LEN + 1; i++)
C[0][i] = 0;
for (int i = 0; i < STR2_LEN + 1; i++)
C[i][0] = 0;
}

int LCS(char* str1, char* str2, int i, int j)
{
if (-1 != C[j][i])
return C[j][i];

if (str1[i-1] == str2[j-1])
C[j][i] = LCS(str1, str2, i - 1, j - 1) + 1;
else
{
int temp1 = LCS(str1, str2, i - 1, j);
int temp2 = LCS(str1, str2, i, j - 1);
C[j][i] = temp1 > temp2 ? temp1 : temp2;
}
return C[j][i];
}


这是自顶向下的动态规划。

再用自底向上的动规来做一次。

int C[STR2_LEN + 1][STR1_LEN + 1] = { 0 };

int LCS(char* str1, char* str2, int str1Len, int str2Len)
{
for (int i = 1; i < str2Len + 1; i++)
for (int j = 1; j < str1Len + 1; j++)
{
if (str1[j - 1] == str2[i - 1])
C[i][j] = C[i - 1][j - 1] + 1;
else
C[i][j] = C[i - 1][j] > C[i][j - 1] ? C[i - 1][j] : C[i][j - 1];
}

return C[str2Len][str1Len];
}


可以看到,目前为止 算法的时间复杂度为O(nm),空间复杂度亦为O(nm)。

但是递推C[i][j]时,只用到了同一行和相邻行的值,而对于再早的值,都没有用到。故可以将空间复杂度减至O(min{m,n})。

假设m<n,即str1的长度较小,因此:

int C[2][STR1_LEN + 1] = { 0 };

int LCS(char* str1, char* str2, int str1Len, int str2Len)
{
for (int i = 1; i < str2Len + 1; i++)
for (int j = 1; j < str1Len + 1; j++)
{
if (str1[j - 1] == str2[i - 1])
C[i%2][j] = C[(i-1)%2][j - 1] + 1;
else
C[i % 2][j] = C[(i - 1) % 2][j] > C[i % 2][j - 1] ? C[(i - 1) % 2][j] : C[i % 2][j - 1];
}

return C[1][str1Len];
}


时间复杂度仍为O(nm),但空间复杂度变为了O(min{m,n})。

利用动态规化的缓存中间结果的功效,成功将时间复杂度为指数级别 降至 O(nm),空间复杂度为O(min{m,n})。m和n分别为这两个字符串的长度。

二、最长公共子串

方法都是类似的。公共子序列与公共子串之间的区别就在于,公共子序列不要求在原字符中是连续的。

同理,记C[i, j]=k,表示从str1[i-k+1...i]与str2[j-k+1...j] 每个对应位置均相等时,k所能取的最大值。

注意,下面这块代码只是用来阐述C[i,j]的定义,绝对不能用来在实际中使用

用代码表示为:

k = 0;
while(str1[i-k] == str2[j-k])
k++;
C[i,j] = k;
上面这块代码只具有阐述意义,不要在实际中使用

用通俗的话来讲,C[i,j]表示以str1的i位置与从str2的j位置结尾,所对应的最长公共子串的长度。

动态规化的特点,就是在求C[i,j]时,要用以前求过的C[0...i-1][0...j-1]来快速求出。

自底向上动规:

#include <iostream>
#include <iterator>
#include <string>

#define STR2_LEN 7
#define STR1_LEN 8
int C[STR2_LEN+1][STR1_LEN+1] = { 0 };
int maxLen = 0, offEndStr1 = 0;

int LCSubstring(std::string& str1, std::string& str2)
{
for (unsigned int i = 1; i < str2.length()+1; i++)
for (unsigned int j = 1; j < str1.length() + 1; j++)
{
if (str1[j-1] == str2[i-1])
C[i][j] = C[i - 1][j - 1] + 1;
if (C[i][j] > maxLen)
{
maxLen = C[i][j];
offEndStr1 = j;
}
}
int startIndex = offEndStr1 - maxLen;
std::ostream_iterator<char> myOut(std::cout, " ");
std::copy(str1.begin() + startIndex, str1.begin() + offEndStr1, myOut);
endl(std::cout);
return maxLen;
}


易知,该算法的时间复杂度为O(nm),空间复杂度亦为O(nm)。

从状态转移公式可知,当前状态只与上一行中的值有关。而与再早的值无关。所以可以优化空间复杂度。

和LCS问题一样,空间复杂度可以优化为O(min{m,n}).

当然,我下面直接用的是str1的长度,只是为了省事。

#define STR2_LEN 7
#define STR1_LEN 8
int C[2][STR1_LEN+1] = { 0 };
int maxLen = 0, offEndStr1 = 0;

int LCSubstring(std::string& str1, std::string& str2)
{
for (unsigned int i = 1; i < str2.length()+1; i++)
for (unsigned int j = 1; j < str1.length() + 1; j++)
{
if (str1[j-1] == str2[i-1])
C[i%2][j] = C[(i - 1)%2][j - 1] + 1;
if (C[i%2][j] > maxLen)
{
maxLen = C[i%2][j];
offEndStr1 = j;
}
}
int startIndex = offEndStr1 - maxLen;
std::ostream_iterator<char> myOut(std::cout, " ");
std::copy(str1.begin() + startIndex, str1.begin() + offEndStr1, myOut);
endl(std::cout);
return maxLen;
}


最长公共子串:时间复杂度O(nm),空间复杂度O(min{m,n})。其中,n与m为这两个字符串的长度。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: