最长公共子序列(LCS)问题 Longest Common Subsequence 与最长公告字串 longest common substr
问题描述:字符序列的子序列是指从给定字符序列中随意地(不一定连续)去掉若干个字符(可能一个也不去掉)后所形成的字符序列。令给定的字符序列X=“x0,x1,…,xm-1”,序列Y=“y0,y1,…,yk-1”是X的子序列,存在X的一个严格递增下标序列<i0,i1,…,ik-1>,使得对所有的j=0,1,…,k-1,有xij=yj。例如,X=“ABCBDAB”,Y=“BCDB”是X的一个子序列。
考虑最长公共子序列问题如何分解成子问题,设A=“a0,a1,…,am-1”,B=“b0,b1,…,bm-1”,并Z=“z0,z1,…,zk-1”为它们的最长公共子序列。不难证明有以下性质:
(1) 如果am-1=bn-1,则zk-1=am-1=bn-1,且“z0,z1,…,zk-2”是“a0,a1,…,am-2”和“b0,b1,…,bn-2”的一个最长公共子序列;
(2) 如果am-1!=bn-1,则若zk-1!=am-1,蕴涵“z0,z1,…,zk-1”是“a0,a1,…,am-2”和“b0,b1,…,bn-1”的一个最长公共子序列;
(3) 如果am-1!=bn-1,则若zk-1!=bn-1,蕴涵“z0,z1,…,zk-1”是“a0,a1,…,am-1”和“b0,b1,…,bn-2”的一个最长公共子序列。
这样,在找A和B的公共子序列时,如有am-1=bn-1,则进一步解决一个子问题,找“a0,a1,…,am-2”和“b0,b1,…,bm-2”的一个最长公共子序列;如果am-1!=bn-1,则要解决两个子问题,找出“a0,a1,…,am-2”和“b0,b1,…,bn-1”的一个最长公共子序列和找出“a0,a1,…,am-1”和“b0,b1,…,bn-2”的一个最长公共子序列,再取两者中较长者作为A和B的最长公共子序列。
求解:
引进一个二维数组c[][],用c[i][j]记录X[i]与Y[j] 的LCS 的长度,b[i][j]记录c[i][j]是通过哪一个子问题的值求得的,以决定搜索的方向。
我们是自底向上进行递推计算,那么在计算c[i,j]之前,c[i-1][j-1],c[i-1][j]与c[i][j-1]均已计算出来。此时我们根据X[i] = Y[j]还是X[i] != Y[j],就可以计算出c[i][j]。
问题的递归式写成:
回溯输出最长公共子序列过程:
算法分析:
由于每次调用至少向上或向左(或向上向左同时)移动一步,故最多调用(m + n)次就会遇到i = 0或j = 0的情况,此时开始返回。返回时与递归调用时方向相反,步数相同,故算法时间复杂度为Θ(m + n)。
http://blog.csdn.net/yysdsyl/article/details/4226630
一切没有code的分析都是耍流氓。。。 上code
void printLCS(string str1, string str2, vector<vector<int> >flag, int idx1, int idx2) { if(idx1 == 0 || idx2 == 0 ) return; if(flag[idx1][idx2] == 1) { printLCS(str1, str2,flag, idx1-1, idx2-1); cout << idx1 <<"\t"<< idx2 <<"\t"; cout << str1[idx1-1] <<"\t"<<endl; } else if(flag[idx1][idx2] == 2) printLCS(str1, str2,flag, idx1, idx2-1); else if(flag[idx1][idx2] == 3) printLCS(str1, str2,flag, idx1-1, idx2); } int lcs(string str1, string str2) { const size_t len1 = str1.size(); const size_t len2 = str2.size(); if(len1 == 0 || len2 == 0) return 0; int f[len1 + 1][len2 + 1]; vector<vector<int> >flag; vector<int> tmp; tmp.resize(len2+1); for(size_t i = 0; i<= len1; i++) flag.push_back(tmp); //memset(flag,0,sizeof(flag)); // 1: leftup; 2: left; 3: up for(size_t i = 0; i <= len1; i++) { f[i][0] = 0; } for(size_t i = 0; i <= len2; i++) { f[0][i] = 0; } for(size_t i = 1; i <= len1; i++) { for(size_t j = 1; j <= len2; j++) { if(str1[i-1] == str2[j-1]) { f[i][j] = f[i-1][j-1] + 1; flag[i][j] = 1; } else { f[i][j] = max(f[i][j-1], f[i-1][j]); if(f[i][j-1] > f[i-1][j]) flag[i][j] = 2; else flag[i][j] = 3; } } } #if 0 for(size_t i = 1; i <= len1; i++) { for(size_t j = 1; j <= len2; j++) { //cout << "f["<<j<<"][" <<i<<"]" << f[j][i] <<"\n"; cout << f[i][j] <<"\t"; } cout << endl; } cout << endl; for(size_t i = 1; i <= len1; i++) { for(size_t j = 1; j <= len2; j++) { //cout << "f["<<j<<"][" <<i<<"]" << f[j][i] <<"\n"; cout << flag[i][j] <<"\t"; } cout << endl; } #endif printLCS(str1, str2, flag, len1, len2); return f[len1][len2]; }
最长公共字串:
找两个字符串的最长公共子串,这个子串要求在原字符串中是连续的。其实这又是一个序贯决策问题,可以用动态规划来求解。我们采用一个二维矩阵来记录中间的结果。这个二维矩阵怎么构造呢?直接举个例子吧:"bab"和"caba"(当然我们现在一眼就可以看出来最长公共子串是"ba"或"ab")
b a b
c 0 0 0
a 0 1 0
b 1 0 1
a 0 1 0
我们看矩阵的斜对角线最长的那个就能找出最长公共子串。
不过在二维矩阵上找最长的由1组成的斜对角线也是件麻烦费时的事,下面改进:当要在矩阵是填1时让它等于其左上角元素加1。
b a b
c 0 0 0
a 0 1 0
b 1 0 2
a 0 2 0
这样矩阵中的最大元素就是 最长公共子串的长度。
在构造这个二维矩阵的过程中由于得出矩阵的某一行后其上一行就没用了,所以实际上在程序中可以用一维数组来代替这个矩阵。
与Subsequence问题不同的是,Substring问题不光要求下标序列是递增的,还要求每次
递增的增量为1, 即两个下标序列为:
<i, i+1, i+2, ..., i+k-1> 和 <j, j+1, j+2, ..., j+k-1>
类比Subquence问题的动态规划解法,Substring也可以用动态规划解决,令
c[i][j]表示Xi和Yi的最大Substring的长度,比如
X = <y, e, d, f>
Y = <y, e, k, f>
c[1][1] = 1
c[2][2] = 2
c[3][3] = 0
c[4][4] = 1
动态转移方程为:
如果xi == yj, 则 c[i][j] = c[i-1][j-1]+1
如果xi ! = yj, 那么c[i][j] = 0
最后求Longest Common Substring的长度等于
max{ c[i][j], 1<=i<=n, 1<=j<=m}
完整的代码如下:
/** 找出两个字符串的最长公共连续子串的长度 ** author :liuzhiwei ** data :2011-08-16 **/ #include "stdio.h" #include "string.h" #include "stdlib.h" int longest_common_substring(char *str1, char *str2) { int i,j,k,len1,len2,max,x,y; len1 = strlen(str1); len2 = strlen(str2); int **c = new int*[len1+1]; for(i = 0; i < len1+1; i++) c[i] = new int[len2+1]; for(i = 0; i < len1+1; i++) c[i][0]=0; //第0列都初始化为0 for(j = 0; j < len2+1; j++) c[0][j]=0; //第0行都初始化为0 max = -1; for(i = 1 ; i < len1+1 ; i++) { for(j = 1; j < len2+1; j++) { if(str1[i-1]==str2[j-1]) //只需要跟左上方的c[i-1][j-1]比较就可以了 c[i][j]=c[i-1][j-1]+1; else //不连续的时候还要跟左边的c[i][j-1]、上边的c[i-1][j]值比较,这里不需要 c[i][j]=0; if(c[i][j]>max) { max=c[i][j]; x=i; y=j; } } } //输出公共子串 char s[1000]; k=max; i=x-1,j=y-1; s[k--]='\0'; while(i>=0 && j>=0) { if(str1[i]==str2[j]) { s[k--]=str1[i]; i--; j--; } else //只要有一个不相等,就说明相等的公共字符断了,不连续了 break; } printf("最长公共子串为:"); puts(s); for(i = 0; i < len1+1; i++) //释放动态申请的二维数组 delete[] c[i]; delete[] c; return max; } int main(void) { char str1[1000],str2[1000]; printf("请输入第一个字符串:"); gets(str1); printf("请输入第二个字符串:"); gets(str2); int len = longest_common_substring(str1, str2); printf("最长公共连续子串的长度为:%d\n",len); system("pause"); return 0; }