动态规划——最长公共子序列问题

来源:互联网 发布:康丽鲨牌灵软胶囊知乎 编辑:程序博客网 时间:2024/05/22 03:43

动态规划算法与分治算法类似,基本思想都是将待解问题分解成若干个子问题,先求解子问题,然后根据子问题的解得到原问题的解。

在用分治法求解的时候有些子问题被重复计算了很多次。

动态规划算法则有效的将子问题的解保存起来,在需要的时候再找出来使用,这样就避免了大量的重复计算。

为了达到这样的目的,可以用一个表来记录所有已经解决的子问题的答案。


最长公共子序列问题:

一个给定序列的子序列是在该序列中删除若干元素之后得到的自序列。确切的说

若给定序列 ABCDEFG  , ABC , CDE, ABCDEFG 等都是其自序列,不一定需要连续的

最长公共子序列也就是给定两个序列 X , Y,求这两个最长的相同的子序列。


最长公共子序列的结构有如下表示:
一:最长公共子序列的结构  
设序列X=<x1, x2, …, xm>和Y=<y1, y2, …, yn>的一个最长公共子序列Z=<z1, z2, …, zk>,则:
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=<x1, x2, …, xm-1>,Yn-1=<y1, y2, …, yn-1>,Zk-1=<z1, z2, …, zk-1>。

二:子问题的递归结构

由最长公共子序列问题的最优子结构性质可知,要找出X=<x1, x2, …, xm>和Y=<y1, y2, …, yn>的最长公共子序列,可按以下方式递归地进行:当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=<x1, x2, …, xi>,Yj=<y1, y2, …, yj>。当i=0或j=0时,空序列是Xi和Yj的最长公共子序列,故c[i,j]=0。其他情况下,由定理可建立递归关系如下:


三:计算最优值

直接利用上节节末的递归式,我们将很容易就能写出一个计算c[i,j]的递归算法,但其计算时间是随输入长度指数增长的。由于在所考虑的子问题空间中,总共只有θ(m*n)个不同的子问题,因此,用动态规划算法自底向上地计算最优值能提高算法的效率。

    计算最长公共子序列长度的动态规划算法LCSLength(xlen,ylen,x,y,c,b);以序列X=<x1, x2, …, xm>和Y=<y1, y2, …, yn>作为输入。输出两个数组c[0..m ,0..n]和b[1..m ,1..n]。其中c[i,j]存储Xi与Yj的最长公共子序列的长度,bi,j]记录指示c[i,j]的值是由哪一个子问题的解达[到的,这在构造最长公共子序列时要用到。最后,X和Y的最长公共子序列的长度记录于c[m,n]中。

void LCSLength(int xlen, int ylen, char x[] , char y[], int c[][MAXLEN] , int b[][MAXLEN]) {int i , j;/* c[i][j]代表当前X1X2...Xi和Y1Y2...Yj序列的公共子序列的长度 因为c[i][0]和c[0][j]都代表其中一个序列长度为0,所以公共自序列长度为0*/for(i=0 ; i<xlen ; i++) {c[i][0] = 0;}for(j=0 ; j<ylen ; j++) {c[0][j] = 0;}// 根据不同的情况计算c[i][j]的值for(i=1 ; i<=xlen ; i++) {for(j=1 ; j<=ylen;j++) {if(x[i-1] == y[j-1]) {c[i][j] = c[i-1][j-1] + 1;b[i][j] = 1;} else if(c[i-1][j] > c[i][j-1]) { // 在c中也即为上侧c[i][j] = c[i-1][j];b[i][j] = 2; } else { // 在c中也即为左侧c[i][j] = c[i][j-1];b[i][j] = 3;}}}}

由算法LCSLength计算得到的数组b可用于快速构造序列X=<x1, x2, …, xm>和Y=<y1, y2, …, yn>的最长公共子序列。首先从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)时间,算法LCSLength耗时Ο(mn)。

四:构造最长公共子序列

 下面的算法GetLCS实现根据b的内容打印出Xi与Yj的最长公共子序列。通过算法的调用LCS(b,X,length[X],length[Y]),便可打印出序列X和Y的最长公共子序列。

void GetLCS(int xlen, int ylen, char x[],int b[][MAXLEN]) {// 因为b[i][j]记录了c[i][j]是从那一种子问题的解得到的if(xlen == 0 || ylen == 0) {return ;}if(b[xlen][ylen] == 1) {GetLCS(xlen-1,ylen-1,x,b);printf("%c",x[xlen-1]);} else if(b[xlen][ylen] == 2) {GetLCS(xlen-1,ylen,x,b);} else {GetLCS(xlen,ylen-1,x,b);}}
在算法LCS中,每一次的递归调用使i或j减1,因此算法的计算时间为O(m+n)。
例如,设所给的两个序列为X=<A,B,C,B,D,A,B>和Y=<B,D,C,A,B,A>。由算法LCS_LENGTH和LCS计算出的结果如下图所示:

                           

我来说明下此图(参考算法导论)。在序列X={A,B,C,B,D,A,B}和 Y={B,D,C,A,B,A}上,由LCSLength计算出的表c和b。第i行和第j列中的方块包含了c[i,j]的值以及指向b[i,j]的箭头。在c[7,6]的项4,表的右下角为X和Y的一个LCS<B,C,B,A>的长度。对于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的成员的项(高亮标示)。

    所以根据上述图所示的结果,程序将最终输出:“B C B A”。


全部代码如下:

#include<stdio.h>#include<string.h>#define MAXLEN 30void LCSLength(int xlen, int ylen, char x[] , char y[], int c[][MAXLEN] , int b[][MAXLEN]) {int i , j;/* c[i][j]代表当前X1X2...Xi和Y1Y2...Yj序列的公共子序列的长度 因为c[i][0]和c[0][j]都代表其中一个序列长度为0,所以公共自序列长度为0*/for(i=0 ; i<xlen ; i++) {c[i][0] = 0;}for(j=0 ; j<ylen ; j++) {c[0][j] = 0;}// 根据不同的情况计算c[i][j]的值for(i=1 ; i<=xlen ; i++) {for(j=1 ; j<=ylen;j++) {if(x[i-1] == y[j-1]) {c[i][j] = c[i-1][j-1] + 1;b[i][j] = 1;} else if(c[i-1][j] > c[i][j-1]) { // 在c中也即为上侧c[i][j] = c[i-1][j];b[i][j] = 2; } else { // 在c中也即为左侧c[i][j] = c[i][j-1];b[i][j] = 3;}}}}void GetLCS(int xlen, int ylen, char x[],int b[][MAXLEN]) {// 因为b[i][j]记录了c[i][j]是从那一种子问题的解得到的if(xlen == 0 || ylen == 0) {return ;}if(b[xlen][ylen] == 1) {GetLCS(xlen-1,ylen-1,x,b);printf("%c",x[xlen-1]);} else if(b[xlen][ylen] == 2) {GetLCS(xlen-1,ylen,x,b);} else {GetLCS(xlen,ylen-1,x,b);}}int main() {int i , j;char x[MAXLEN] , y[MAXLEN];int c[MAXLEN][MAXLEN] , b[MAXLEN][MAXLEN];// m记录X序列的长度,n记录Y序列的长度int xlen , ylen;// 分别输入X,Y序列printf("请输入X序列:\n");scanf("%s",x);xlen = strlen(x);printf("请输入y序列:\n");scanf("%s",y);ylen = strlen(y);LCSLength(xlen,ylen,x,y,c,b);printf("数组c的结果为:\n");for(i=1;i<=xlen;i++) {for(j=1;j<=ylen;j++) {printf("%3d",c[i][j]);}printf("\n");}printf("数组b的结果为:\n");for(i=1;i<=xlen;i++) {for(j=1;j<=ylen;j++) {printf("%3d",b[i][j]);}printf("\n");}printf("公共子序列长度为:%d\n", c[xlen][ylen]);printf("公共子序列为:");GetLCS(xlen,ylen,x,b);printf("\n"); return 0;}

以上原理述说和图片参考自:http://blog.csdn.net/v_july_v/article/details/6695482