lcs算法详解.docx

上传人:b****7 文档编号:11410830 上传时间:2023-02-28 格式:DOCX 页数:14 大小:150.74KB
下载 相关 举报
lcs算法详解.docx_第1页
第1页 / 共14页
lcs算法详解.docx_第2页
第2页 / 共14页
lcs算法详解.docx_第3页
第3页 / 共14页
lcs算法详解.docx_第4页
第4页 / 共14页
lcs算法详解.docx_第5页
第5页 / 共14页
点击查看更多>>
下载资源
资源描述

lcs算法详解.docx

《lcs算法详解.docx》由会员分享,可在线阅读,更多相关《lcs算法详解.docx(14页珍藏版)》请在冰豆网上搜索。

lcs算法详解.docx

lcs算法详解

程序员编程艺术第十一章:

最长公共子序列(LCS)问题

0、前言

   程序员编程艺术系列重新开始创作了(前十章,请参考程序员编程艺术第一~十章集锦与总结)。

回顾之前的前十章,有些代码是值得商榷的,因当时的代码只顾阐述算法的原理或思想,所以,很多的与代码规范相关的问题都未能做到完美。

日后,会着力修缮之。

   搜遍网上,讲解这个LCS问题的文章不计其数,但大多给读者一种并不友好的感觉,稍感晦涩,且代码也不够清晰。

本文力图避免此些情况。

力保通俗,阐述详尽。

同时,经典算法研究系列的第三章(三、dynamicprogramming)也论述了此LCS问题。

有任何问题,欢迎不吝赐教。

第一节、问题描述

   什么是最长公共子序列呢?

好比一个数列 S,如果分别是两个或多个已知数列的子序列,且是所有符合此条件序列中最长的,则S 称为已知序列的最长公共子序列。

   举个例子,如:

有两条随机序列,如13455,and245576,则它们的最长公共子序列便是:

455。

   注意最长公共子串(LongestCommonSubstring)和最长公共子序列(LongestCommonSubsequence,LCS)的区别:

子串(Substring)是串的一个连续的部分,子序列(Subsequence)则是从不改变序列的顺序,而从序列中去掉任意的元素而获得的新序列;更简略地说,前者(子串)的字符的位置必须连续,后者(子序列LCS)则不必。

比如字符串acdfg同akdfc的最长公共子串为df,而他们的最长公共子序列是adf。

LCS可以使用动态规划法解决。

下文具体描述。

第二节、LCS问题的解决思路

∙穷举法   

   解最长公共子序列问题时最容易想到的算法是穷举搜索法,即对X的每一个子序列,检查它是否也是Y的子序列,从而确定它是否为X和Y的公共子序列,并且在检查过程中选出最长的公共子序列。

X和Y的所有子序列都检查过后即可求出X和Y的最长公共子序列。

X的一个子序列相应于下标序列{1,2,…,m}的一个子序列,因此,X共有2m个不同子序列(Y亦如此,如为2^n),从而穷举搜索法需要指数时间(2^m*2^n)。

∙动态规划算法

   事实上,最长公共子序列问题也有最优子结构性质。

记:

Xi=﹤x1,⋯,xi﹥即X序列的前i个字符(1≤i≤m)(前缀)

Yj=﹤y1,⋯,yj﹥即Y序列的前j个字符(1≤j≤n)(前缀)

假定Z=﹤z1,⋯,zk﹥∈LCS(X,Y)。

∙若xm=yn(最后一个字符相同),则不难用反证法证明:

该字符必是X与Y的任一最长公共子序列Z(设长度为k)的最后一个字符,即有zk=xm=yn且显然有Zk-1∈LCS(Xm-1,Yn-1)即Z的前缀Zk-1是Xm-1与Yn-1的最长公共子序列。

此时,问题化归成求Xm-1与Yn-1的LCS(LCS(X,Y)的长度等于LCS(Xm-1,Yn-1)的长度加1)。

∙若xm≠yn,则亦不难用反证法证明:

要么Z∈LCS(Xm-1,Y),要么Z∈LCS(X,Yn-1)。

由于zk≠xm与zk≠yn其中至少有一个必成立,若zk≠xm则有Z∈LCS(Xm-1,Y),类似的,若zk≠yn则有Z∈LCS(X,Yn-1)。

此时,问题化归成求Xm-1与Y的LCS及X与Yn-1的LCS。

LCS(X,Y)的长度为:

max{LCS(Xm-1,Y)的长度,LCS(X,Yn-1)的长度}。

   由于上述当xm≠yn的情况中,求LCS(Xm-1,Y)的长度与LCS(X,Yn-1)的长度,这两个问题不是相互独立的:

两者都需要求LCS(Xm-1,Yn-1)的长度。

另外两个序列的LCS中包含了两个序列的前缀的LCS,故问题具有最优子结构性质考虑用动态规划法。

   也就是说,解决这个LCS问题,你要求三个方面的东西:

1、LCS(Xm-1,Yn-1)+1;2、LCS(Xm-1,Y),LCS(X,Yn-1);3、max{LCS(Xm-1,Y),LCS(X,Yn-1)}。

   行文至此,其实对这个LCS的动态规划解法已叙述殆尽,不过,为了成书的某种必要性,下面,我试着再多加详细阐述这个问题。

第三节、动态规划算法解LCS问题

3.1、最长公共子序列的结构

   最长公共子序列的结构有如下表示:

   设序列X=和Y=的一个最长公共子序列Z=,则:

1.若xm=yn,则zk=xm=yn且Zk-1是Xm-1和Yn-1的最长公共子序列;

2.若xm≠yn且zk≠xm,则Z是Xm-1和Y的最长公共子序列;

3.若xm≠yn且zk≠yn ,则Z是X和Yn-1的最长公共子序列。

   其中Xm-1=,Yn-1=,Zk-1=

3、2.子问题的递归结构

   由最长公共子序列问题的最优子结构性质可知,要找出X=和Y=的最长公共子序列,可按以下方式递归地进行:

当xm=yn时,找出Xm-1和Yn-1的最长公共子序列,然后在其尾部加上xm(=yn)即可得X和Y的一个最长公共子序列。

当xm≠yn时,必须解两个子问题,即找出Xm-1和Y的一个最长公共子序列及X和Yn-1的一个最长公共子序列。

这两个公共子序列中较长者即为X和Y的一个最长公共子序列。

   由此递归结构容易看到最长公共子序列问题具有子问题重叠性质。

例如,在计算X和Y的最长公共子序列时,可能要计算出X和Yn-1及Xm-1和Y的最长公共子序列。

而这两个子问题都包含一个公共子问题,即计算Xm-1和Yn-1的最长公共子序列。

   与矩阵连乘积最优计算次序问题类似,我们来建立子问题的最优值的递归关系。

用c[i,j]记录序列Xi和Yj的最长公共子序列的长度。

其中Xi=,Yj=

当i=0或j=0时,空序列是Xi和Yj的最长公共子序列,故c[i,j]=0。

其他情况下,由定理可建立递归关系如下:

3、3.计算最优值

   直接利用上节节末的递归式,我们将很容易就能写出一个计算c[i,j]的递归算法,但其计算时间是随输入长度指数增长的。

由于在所考虑的子问题空间中,总共只有θ(m*n)个不同的子问题,因此,用动态规划算法自底向上地计算最优值能提高算法的效率。

   计算最长公共子序列长度的动态规划算法LCS_LENGTH(X,Y)以序列X=和Y=作为输入。

输出两个数组c[0..m,0..n]和b[1..m,1..n]。

其中c[i,j]存储Xi与Yj的最长公共子序列的长度,b[i,j]记录指示c[i,j]的值是由哪一个子问题的解达到的,这在构造最长公共子序列时要用到。

最后,X和Y的最长公共子序列的长度记录于c[m,n]中。

1.Procedure LCS_LENGTH(X,Y);  

2.begin  

3.  m:

=length[X];  

4.  n:

=length[Y];  

5.  for i:

=1 to m do c[i,0]:

=0;  

6.  for j:

=1 to n do c[0,j]:

=0;  

7.  for i:

=1 to m do  

8.    for j:

=1 to n do  

9.      if x[i]=y[j] then  

10.        begin  

11.          c[i,j]:

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

12.          b[i,j]:

="↖";  

13.        end  

14.      else if c[i-1,j]≥c[i,j-1] then  

15.        begin  

16.          c[i,j]:

=c[i-1,j];  

17.          b[i,j]:

="↑";  

18.        end  

19.      else  

20.        begin  

21.          c[i,j]:

=c[i,j-1];  

22.          b[i,j]:

="←"  

23.        end;  

24.  return(c,b);  

25.end;   

   由算法LCS_LENGTH计算得到的数组b可用于快速构造序列X=和Y=的最长公共子序列。

首先从b[m,n]开始,沿着其中的箭头所指的方向在数组b中搜索。

∙当b[i,j]中遇到"↖"时(意味着xi=yi是LCS的一个元素),表示Xi与Yj的最长公共子序列是由Xi-1与Yj-1的最长公共子序列在尾部加上xi得到的子序列;

∙当b[i,j]中遇到"↑"时,表示Xi与Yj的最长公共子序列和Xi-1与Yj的最长公共子序列相同;

∙当b[i,j]中遇到"←"时,表示Xi与Yj的最长公共子序列和Xi与Yj-1的最长公共子序列相同。

   这种方法是按照反序来找LCS的每一个元素的。

由于每个数组单元的计算耗费Ο

(1)时间,算法LCS_LENGTH耗时Ο(mn)。

3、4.构造最长公共子序列

   下面的算法LCS(b,X,i,j)实现根据b的内容打印出Xi与Yj的最长公共子序列。

通过算法的调用LCS(b,X,length[X],length[Y]),便可打印出序列X和Y的最长公共子序列。

1.Procedure LCS(b,X,i,j);  

2.begin  

3.  if i=0 or j=0 then return;  

4.  if b[i,j]="↖" then  

5.    begin  

6.      LCS(b,X,i-1,j-1);  

7.      print(x[i]); {打印x[i]}  

8.    end  

9.  else if b[i,j]="↑" then LCS(b,X,i-1,j)   

10.                      else LCS(b,X,i,j-1);  

11.end;   

在算法LCS中,每一次的递归调用使i或j减1,因此算法的计算时间为O(m+n)。

例如,设所给的两个序列为X=和Y=

由算法LCS_LENGTH和LCS计算出的结果如下图所示:

    我来说明下此图(参考算法导论)。

在序列X={A,B,C,B,D,A,B}和Y={B,D,C,A,B,A}上,由LCS_LENGTH计算出的表c和b。

第i行和第j列中的方块包含了c[i,j]的值以及指向b[i,j]的箭头。

在c[7,6]的项4,表的右下角为X和Y的一个LCS的长度。

对于i,j>0,项c[i,j]仅依赖于是否有xi=yi,及项c[i-1,j]和c[i,j-1]的值,这几个项都在c[i,j]之前计算。

为了重构一个LCS的元素,从右下角开始跟踪b[i,j]的箭头即可,这条路径标示为阴影,这条路径上的每一个“↖”对应于一个使xi=yi为一个LCS的成员的项(高亮标示)。

   所以根据上述图所示的结果,程序将最终输出:

“BCBA”。

3、5.算法的改进

   对于一个具体问题,按照一般的算法设计策略设计出的算法,往往在算法的时间和空间需求上还可以改进。

这种改进,通常是利用具体问题的一些特殊性。

   例如,在算法LCS_LENGTH和LCS中,可进一步将数组b省去。

事实上,数组元素c[i,j]的值仅由c[i-1,j-1],c[i-1,j]和c[i,j-1]三个值之一确定,而数组元素b[i,j]也只是用来指示c[i,j]究竟由哪个值确定。

因此,在算法LCS中,我们可以不借助于数组b而借助于数组c本身临时判断c[i,j]的值是由c[i-1,j-1],c[i-1,j]和c[i,j-1]中哪一个数值元素所确定,代价是Ο

(1)时间。

既然b对于算法LCS不是必要的,那么算法LCS_LENGTH便不必保存它。

这一来,可节省θ(mn)的空间,而LCS_LENGTH和LCS所需要的时间分别仍然是Ο(mn)和Ο(m+n)。

不过,由于数组c仍需要Ο(mn)的空间,因此这里所作的改进,只是在空间复杂性的常数因子上的改进。

   另外,如果只需要计算最长公共子序列的长度,则算法的空间需求还可大大减少。

事实上,在计算c[i,j]时,只用到数组c的第i行和第i-1行。

因此,只要用2行的数组空间就可以计算出最长公共子序列的长度。

更进一步的分析还可将空间需求减至min(m,n)。

第四节、编码实现LCS问题

   动态规划的一个计算最长公共子序列的方法如下,以两个序列 X、Y 为例子:

设有二维数组 f[i][j] 表示 X 的 i 位和 Y 的 j 位之前的最长公共子序列的长度,则有:

f[1][1]= same(1,1)

f[i][j]= max{f[i −1][j −1]+same(i,j), f[i −1][j] ,f[i][j −1]}

其中,same(a,b)当 X 的第 a 位与 Y 的第 b 位完全相同时为“1”,否则为“0”。

此时,f[i][j]中最大的数便是 X 和 Y 的最长公共子序列的长度,依据该数组回溯,便可找出最长公共子序列。

该算法的空间、时间复杂度均为O(n2),经过优化后,空间复杂度可为O(n),时间复杂度为O(nlogn)。

以下是此算法的java代码:

1.   

2.import java.util.Random;  

3.   

4.public class LCS{  

5.    public static void main(String[] args){  

6.   

7.        //设置字符串长度  

8.        int substringLength1 = 20;  

9.        int substringLength2 = 20;  //具体大小可自行设置  

10.   

11.        // 随机生成字符串  

12.        String x = GetRandomStrings(substringLength1);  

13.        String y = GetRandomStrings(substringLength2);  

14.   

15.        Long startTime = System.nanoTime();  

16.        // 构造二维数组记录子问题x[i]和y[i]的LCS的长度  

17.        int[][] opt = new int[substringLength1 + 1][substringLength2 + 1];  

18.   

19.        // 动态规划计算所有子问题  

20.        for (int i = substringLength1 - 1; i >= 0; i--){  

21.            for (int j = substringLength2 - 1; j >= 0; j--){  

22.                if (x.charAt(i) == y.charAt(j))  

23.                    opt[i][j] = opt[i + 1][j + 1] + 1;                                 //参考上文我给的公式。

  

24.                else  

25.                    opt[i][j] = Math.max(opt[i + 1][j], opt[i][j + 1]);        //参考上文我给的公式。

  

26.            }  

27.        }  

28.   

29.        -------------------------------------------------------------------------------------  

30.   

31.        理解上段,参考上文我给的公式:

  

32.   

33.        根据上述结论,可得到以下公式,  

34.   

35.        如果我们记字符串Xi和Yj的LCS的长度为c[i,j],我们可以递归地求c[i,j]:

  

36.   

37.                  /      0                               if i<0 or j<0  

38.        c[i,j]=          c[i-1,j-1]+1                    if i,j>=0 and xi=xj  

39.                 /       max(c[i,j-1],c[i-1,j]           if i,j>=0 and xi≠xj  

40.   

41.        -------------------------------------------------------------------------------------  

42.   

43.        System.out.println("substring1:

"+x);  

44.        System.out.println("substring2:

"+y);  

45.        System.out.print("LCS:

");  

46.   

47.        int i = 0, j = 0;  

48.        while (i < substringLength1 && j < substringLength2){  

49.            if (x.charAt(i) == y.charAt(j)){  

50.                System.out.print(x.charAt(i));  

51.                i++;  

52.                j++;  

53.            } else if (opt[i + 1][j] >= opt[i][j + 1])  

54.                i++;  

55.            else  

56.                j++;  

57.        }  

58.        Long endTime = System.nanoTime();  

59.        System.out.println(" Totle time is " + (endTime - startTime) + " ns");  

60.    }  

61.   

62.    //取得定长随机字符串  

63.    public static String GetRandomStrings(int length){  

64.        StringBuffer buffer = new StringBuffer("abcdefghijklmnopqrstuvwxyz");  

65.        StringBuffer sb = new StringBuffer();  

66.        Random r = new Random();  

67.        int range = buffer.length();  

68.        for (int i = 0; i < length; i++){  

69.            sb.append(buffer.charAt(r.nextInt(range)));  

70.        }  

71.        return sb.toString();  

72.    }  

73.}  

第五节、改进的算法

   下面咱们来了解一种不同于动态规划法的一种新的求解最长公共子序列问题的方法,该算法主要是把求解公共字符串问题转化为求解矩阵L(p,m)的问题,在利用定理求解矩阵的元素过程中

(1)while(i

                 

(2)while(L(k,i)=k),L(k,i+1)=L(k,i+2)=…L(k,m)=k;

   求出每列元素,一直到发现第p+1行都为null时退出循环,得出矩阵L(k,m)后,B[L(1,m-p+1)]B[L(2,m-p+2)]…B[L(p,m)]即为A和B的LCS,其中p为LCS的长度。

6.1主要定义及定理

∙定义1子序列(Subsequence):

给定字符串A=A[1]A[2]…A[m],(A[i]是A的第i个字母,A[i]∈字符集Σ,l<=i

∙定义2公共子序列(CommonSubsequence):

给定字符串A、B、C,C称为A和B的公共子序列是指C既是A的子序列,又是B的子序列。

∙定义3最长公共子序列(LongestCommonSubsequence简称LCS):

给定字符串A、B、C,C称为A和B的最长公共子序列是指C是A和B的公共子序列,且对于A和B的任意公共子序列D,都有D<=C。

给定字符串A和B,A=m,B=n,不妨设m<=n,LCS问题就是要求出A和B的LCS。

∙定义4给定字符串A=A[1]A[2]…A[m]和字符串B=B[1]B[2]…[n],A(1:

i)表示A的连续子序列A[1]A[2]…A[i],同样B(1:

j)表示B的连续子序列B[1]B[2]…[j]。

Li(k)表示所有与字符串A(1:

i)有长度为k的LCS的字符串B(l:

j)中j的最小值。

用公式表示就是Li(k)=Minj(LCS(A(1:

i),B(l:

j))=k)[3]。

定理1∀i∈[1,m],有Li(l)

(2)

定理2∀i∈[l,m-1],∀k∈[l,m],有i1L+(k)<=iL(k).

定理3∀i∈[l,m-1],∀k∈[l,m-l],有iL(k)

以上三个定理都不考虑Li(k)无定义的情况。

定理4[3]i1L

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 高等教育 > 医学

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1