最长公共子序列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对应。
这是自顶向下的动态规划。
再用自底向上的动规来做一次。
可以看到,目前为止 算法的时间复杂度为O(nm),空间复杂度亦为O(nm)。
但是递推C[i][j]时,只用到了同一行和相邻行的值,而对于再早的值,都没有用到。故可以将空间复杂度减至O(min{m,n})。
假设m<n,即str1的长度较小,因此:
时间复杂度仍为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]的定义,绝对不能用来在实际中使用
用代码表示为:
用通俗的话来讲,C[i,j]表示以str1的i位置与从str2的j位置结尾,所对应的最长公共子串的长度。
动态规化的特点,就是在求C[i,j]时,要用以前求过的C[0...i-1][0...j-1]来快速求出。
自底向上动规:
易知,该算法的时间复杂度为O(nm),空间复杂度亦为O(nm)。
从状态转移公式可知,当前状态只与上一行中的值有关。而与再早的值无关。所以可以优化空间复杂度。
和LCS问题一样,空间复杂度可以优化为O(min{m,n}).
当然,我下面直接用的是str1的长度,只是为了省事。
最长公共子串:时间复杂度O(nm),空间复杂度O(min{m,n})。其中,n与m为这两个字符串的长度。
最长公共子序列的问题常用于解决字符串的相似度。
最长公共子序列全称为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为这两个字符串的长度。
相关文章推荐
- 正则替换全角空格和逗号
- 创建SQLAlchemy的ORM类的基类(二)
- Android解析XML文件
- 图像归一化,减均值预处理
- SQL*LOADER错误总结
- 【js类库AngularJs】解决angular+springmvc的post提交问题
- POJ 1442 Black Box
- MySQL Nested-Loop Join算法学习
- Python——迭代器和解析(3)
- Duplicate files copied in APK
- 【学神-RHEL7】1-13-MBR磁盘管理
- 课时6第二章:流程控制2
- spring mvc +bootstrap+datatable实现分页
- 关于javascript 以及 jQuery中获取文本值得一点看法
- C语言学习总结
- kettle 如何使用java代码
- 欢迎使用CSDN-markdown编辑器
- Android 视频刻录
- 数字签名(以ActiveXDemo为例)
- Yocto开发笔记之《应用程序架构》(QQ交流群:519230208)