【转帖】最长公共子序列

     转自:http://zhedahht.blog.163.com/blog/static/254111742007376431815/

   题目:如果字符串一的所有字符按其在字符串中的顺序出现在另外一个字符串二中,则字符串一称之为字符串二的子串。注意,并不要求子串(字符串一)的字符必须连续出现在字符串二中。请编写一个函数,输入两个字符串,求它们的最长公共子串,并打印出最长公共子串例如:输入两个字符串BDCABAABCBDAB,字符串BCBA和BDAB都是是它们的最长公共子串,则输出它们的长度4,并打印任意一个子串。

   分析:求最长公共子串(Longest Common Subsequence, LCS)是一道非常经典的动态规划题,因此一些重视算法的公司像MicroStrategy都把它当作面试题。完整介绍动态规划将需要很长的篇幅,因此我不打算在此全面讨论动态规划相关的概念,只集中对LCS直接相关内容作讨论。如果对动态规划不是很熟悉,请参考相关算法书比如算法讨论。

    先介绍LCS问题的性质:记Xm={x0, x1,…xm-1}和Yn={y0,y1,…,yn-1}为两个字符串,而Zk={z0,z1,…zk-1}是它们的LCS,则:

   (1)如果xm-1=yn-1,那么zk-1=xm-1=yn-1,并且Zk-1是Xm-1和Yn-1的LCS;

   (2)如果xm-1≠yn-1,那么当zk-1≠xm-1时Z是Xm-1和Y的LCS;

   (3)如果xm-1≠yn-1,那么当zk-1≠yn-1时Z是Yn-1和X的LCS;

    有了上述性质,可得出如下思路:求两字符串Xm={x0, x1,…xm-1}和Yn={y0,y1,…,yn-1}的LCS,如果xm-1=yn-1,只需求得Xm-1和Yn-1的LCS,并在其后添加xm-1(yn-1)即可;如果xm-1≠yn-1,分别求得Xm-1和Y的LCS和Yn-1和X的LCS,并且这两个LCS中较长的一个为X和Y的LCS。如果我们记字符串Xi和Yj的LCS的长度为c[i,j],可以递归地求c[i,j]:

             /      0                               if i<0 or j<0
   c[i,j]=          c[i-1,j-1]+1                    if i,j>=0 and xi=x
j
            \       max(c[i,j-1],c[i-1,j]           if i,j>=0 and xi≠xj

    上面的公式用递归函数不难求得,但从前面求Fibonacci第n项(本面试题系列第16题)的分析中我们知道直接递归会有很多重复计算,用从底向上循环求解的思路效率更高。

    为了能够采用循环求解的思路,我们用一个矩阵(参考代码中的LCS_length)保存下来当前已经计算好了的c[i,j],当后面的计算需要这些数据时就可以直接从矩阵读取。另外,求取c[i,j]可以从c[i-1,j-1] 、c[i,j-1]或者c[i-1,j]三个方向计算得到,相当于在矩阵LCS_length中是从c[i-1,j-1],c[i,j-1]或者c[i-1,j]的某一个各自移动到c[i,j],因此在矩阵中有三种不同的移动方向:向左、向上和向左上方,其中只有向左上方移动时才表明找到LCS中的一个字符。于是我们需要用另外一个矩阵(参考代码中的LCS_direction)保存移动的方向。参考代码如下:

 

01    #include<iostream>
02 #include<cstring>
03 #include<stack>
04 #include<utility>
05 #define LEFTUP 0
06 #define LEFT 1
07 #define UP 2
08 using namespace std;
09 int Max(int a,int b,int c,int *max){ //找最大者时a的优先级别最高,c的最低.最大值保存在*max中
10 int res=0; //res记录来自于哪个单元格
11 *max=a;
12 if(b>*max){
13 *max=b;
14 res=1;
15 }
16 if(c>*max){
17 *max=c;
18 res=2;
19 }
20 return res;
21 }
22 //调用此函数时请注意把较长的字符串赋给str1,这主要是为了在回溯最长子序列时节省时间。如果没有把较长的字符串赋给str1不影响程序的正确执行。
23 string LCS(const string &str1,const string &str2){
24 int xlen=str1.size(); //横向长度
25 int ylen=str2.size(); //纵向长度
26 if(xlen==0||ylen==0) //str1和str2中只要有一个为空,则返回空
27 return "";
28 pair<int,int> arr[ylen+1][xlen+1]; //构造pair二维数组,first记录数据,second记录来源
29 for(int i=0;i<=xlen;i++) //首行清0
30 arr[0][i].first=0;
31 for(int j=0;j<=ylen;j++) //首列清0
32 arr[j][0].first=0;
33 for(int i=1;i<=ylen;i++){
34 char s=str2.at(i-1);
35 for(int j=1;j<=xlen;j++){
36 int leftup=arr[i-1][j-1].first;
37 int left=arr[i][j-1].first;
38 int up=arr[i-1][j].first;
39 if(str1.at(j-1)==s) //C1==C2
40 leftup++;
41 int max;
42 arr[i][j].second=Max(leftup,left,up,&arr[i][j].first);
43 // cout<<arr[i][j].first<<arr[i][j].second<<" ";
44 }
45 // cout<<endl;
46 } /*矩阵构造完毕*/
47 //回溯找出最长公共子序列
48 stack<int> st;
49 int i=ylen,j=xlen;
50 while(!(i==0&&j==0)){
51 if(arr[i][j].second==LEFTUP){
52 if(arr[i][j].first==arr[i-1][j-1].first+1)
53 st.push(i);
54 --i;
55 --j;
56 }
57 else if(arr[i][j].second==LEFT){
58 --j;
59 }
60 else if(arr[i][j].second==UP){
61 --i;
62 }
63 }
64 string res="";
65 while(!st.empty()){
66 int index=st.top()-1;
67 res.append(str2.substr(index,1));
68 st.pop();
69 }
70 return res;
71 }
72 int main(){
73 string str1="GCCCTAGCG";
74 string str2="GCGCAATG";
75 string lcs=LCS(str1,str2);
76 cout<<lcs<<endl;
77 return 0;
78 }

    扩展:如果题目改成求两个字符串的最长公共子字符串,应该怎么求?子字符串的定义和子串的定义类似,但要求是连续分布在其他字符串中。比如输入两个字符串BDCABAABCBDAB的最长公共字符串有BDAB,它们的长度都是2

 

posted on 2011-10-28 16:47  白草黒尖  阅读(205)  评论(0编辑  收藏  举报