百度百科

  一个序列 S ,如果分别是两个或多个已知序列的子序列,

  且是所有符合此条件序列中最长的,则 S 称为已知序列的最长公共子序列。

注意:S在已知序列中可以不连续;比如ABCBDAB和BDCABA的LCS为BCBA,BCBA不连续出现。

LCS通常利用动态规划算法求解,最简单的求解方式如《算法导论》所述:

  1.设输入为X1X2…Xm,Y1Y2…Yn,构造二维数组B、C;

     其中B[i][j]存储C[i][j]的最优解指向,C[i][j]存储X[1…i]和Y[1…j]的LCS长度;

  2.B、C数组同时更新:

    2.1.令B[0][k]、C[0][k](k=0…n)和B[k][0]、C[k][0](k=0…m)为0;

    2.2.按下标从小到大的顺序依次计算每行的B[i][j]和C[i][j],规则如下:

      ①.若X[i]=Y[j],则C[i][j]=C[i-1][j-1]+1,B[i][j]=0;

      ②.若X[i]!=Y[j],则C[i][j]=Max(C[i][j-1],C[i-1][j]),B[i][j]=1或2;

  3.更新完B、C后,从C[m][n]开始递归输出,根据B[i][j]的指向打印LCS;

相信大家都明白这个原理,简单的用一张图来说明一下:

  1.箭头代表B[i][j]的值,其中用指向左上方的箭头代表0,←和↑代表1和2(或2和1);

  2.箭头下方的数字为C[i][j]的值,即子问题X[1…i]和Y[1…j]的LCS长度;

可是这样的构造只能输出一个LCS,但是我们发现BCBA、BCAB、BDAB都是该序列集的LCS。

为什么呢?因为该算法在C[i-1][j]=C[i][j-1]时,只记录了一种情况!即没有保存分支!

如何简单的修改算法,以求得多个LCS呢?下面介绍一种改进的方法。

还是利用数组B、C,并且C的计算规则不变,下面修改B的计算规则:

  1.若X[i]=Y[j],B[i][j]=0;

  2.若X[i]!=Y[j],有以下判断:

    ①.若C[i-1][j]>C[i][j-1],则C[i][j]=C[i-1][j],B[i][j]=1;

    ②.若C[i-1][j]<C[i][j-1],则C[i][j]=C[i][j-1],B[i][j]=2;

    ③.若C[i-1][j]=C[i][j-1],则C[i][j]的长度任取,B[i][j]=3;

此时用另一张图表示该算法:

  1.用四种箭头表示B[i][j]的值,其中←和↑都存在时,表示C[i-1][j]=C[i][j-1];

  2.输出的时候采用正则表达式的思想:遇到B[i][j]=3时,同时对两种情况进行递归;

     形式为“(←+↑)”,其中←和↑分别表示两个子问题的LCS;

补充:(A+B),在正则表达式中意义为A或B,即“+”表示二选一的关系。

下面给出C++源代码帮助大家更好地理解:(编译器为Codeblocks)

复制代码
 1 #include<iostream>
 2 #include<string.h>
 3 #define M 100
 4 #define N 100
 5 using namespace std;
 6 
 7 void LCS_length(string x,string y,int B[M][N],int C[M][N])/*动态规划*/
 8 {   for(int i=1;i<=x.length()-1;i++)    C[i][0]=0;
 9     for(int j=0;j<=y.length()-1;j++)    C[0][j]=0;
10     for(int i=1;i<=x.length()-1;i++)
11         for(int j=1;j<=y.length()-1;j++)
12         {   if(x[i]==y[j])
13             {   C[i][j]=C[i-1][j-1]+1;B[i][j]=0/*Up&Left*/;}
14             else if(C[i-1][j]>C[i][j-1])
15             {   C[i][j]=C[i-1][j];B[i][j]=1/*Up*/;}
16             else if(C[i-1][j]<C[i][j-1])
17             {   C[i][j]=C[i][j-1];B[i][j]=2/*Left*/;}
18             else
19             {   C[i][j]=C[i][j-1];B[i][j]=3/*Up+Left*/;}}}
20 
21 void Print_LCS(string x,int B[M][N],int i,int j)/*输出LCS*/
22 {   if(!i||!j)    return;
23     if(!B[i][j])
24     {   Print_LCS(x,B,i-1,j-1);cout<<x[i];}
25     else if(B[i][j]==1)
26         Print_LCS(x,B,i-1,j);
27     else if(B[i][j]==2)
28         Print_LCS(x,B,i,j-1);
29     else
30     {   cout<<'(';
31         Print_LCS(x,B,i-1,j);
32         cout<<'+';
33         Print_LCS(x,B,i,j-1);
34         cout<<')';}}
35 
36 int main()
37 {   string x,y;
38     cin>>x;cin>>y;
39     x=' '+x;y=' '+y;
40     int B[M][N],C[M][N];
41     LCS_length(x,y,B,C);cout<<endl;
42     Print_LCS(x,B,x.length(),y.length());}
复制代码

可以在https://github.com/XueDingWen/Software-Security/blob/master/LCS.cpp下载

看完Print_LCS()函数,大家应该大致有所了解,下面给出运行结果:

  输入为ABCBDAB、BDCABA;

  输出为(BCBA+(BC+BD)AB),即表示BCBA、BCAB、BDAB都为输入的LCS;

当然,如果不想采用这种形式,也可以:

  1.用字符型数组(如Vector)或String型变量保存输出;

  2.根据括号分解,输出多个LCS;

还有一种方式:

  1.遍历数组B,用栈保存B[i][j]=3的点的坐标,比如[7,6],[5,3]等;

  2.让这些点的赋值从11……11逐渐变为22……22;

因为B[i][j]=3可以理解为B[i][j]=1或2,那么用二进制序列的思想遍历每种赋值情况;

当然此时时间开销更大,还可能重复输出,所以建议第一种方式。