第十五章 动态规划——最长公共子序列

1、基本概念

  一个给定序列的子序列就是该给定序列中去掉零个或者多个元素的序列。形式化来讲就是:给定一个序列X={x1,x2,……,xm},另外一个序列Z={z1、z2、……,zk},如果存在X的一个严格递增小标序列<i1,i2……,ik>,使得对所有j=1,2,……k,有xij = zj,则Z是X的子序列。例如:Z={B,C,D,B}是X={A,B,C,B,D,A,B}的一个子序列,相应的小标为<2,3,5,7>。从定义可以看出子序列直接的元素不一定是相邻的。

公共子序列:给定两个序列X和Y,如果Z既是X的一个子序列又是Y的一个子序列,则称序列Z是X和Y的公共子序列。例如:X={A,B,C,B,D,A,B},Y={B,D,C,A,B,A},则序列{B,C,A}是X和Y的一个公共子序列,但不不是最长公共子序列。

最长公共子序列(LCS)问题描述:给定两个序列X={x1,x2,……,xm}和Y={y1,y2,……,yn},找出X和Y的最长公共子序列。

2、动态规划解决过程

1)描述一个最长公共子序列

  如果序列比较短,可以采用蛮力法枚举出X的所有子序列,然后检查是否是Y的子序列,并记录所发现的最长子序列。如果序列比较长,这种方法需要指数级时间,不切实际。

  LCS的最优子结构定理:设X={x1,x2,……,xm}和Y={y1,y2,……,yn}为两个序列,并设Z={z1、z2、……,zk}为X和Y的任意一个LCS,则:

      (1)如果xm=yn,那么zk=xm=yn,而且Zk-1是Xm-1和Yn-1的一个LCS。

  (2)如果xm≠yn,那么zk≠xm蕴含Z是是Xm-1和Yn的一个LCS。

  (3)如果xm≠yn,那么zk≠yn蕴含Z是是Xm和Yn-1的一个LCS。

  定理说明两个序列的一个LCS也包含两个序列的前缀的一个LCS,即LCS问题具有最优子结构性质。

2)一个递归解

  根据LCS的子结构可知,要找序列X和Y的LCS,根据xm与yn是否相等进行判断的,如果xm=yn则产生一个子问题,否则产生两个子问题。设C[i,j]为序列Xi和Yj的一个LCS的长度。如果i=0或者j=0,即一个序列的长度为0,则LCS的长度为0。LCS问题的最优子结构的递归式如下所示:

第十五章 动态规划——最长公共子序列

3)计算LCS的长度

  采用动态规划自底向上计算解。书中给出了求解过程LCS_LENGTH,以两个序列为输入。将计算序列的长度保存到一个二维数组C[M][N]中,另外引入一个二维数组B[M][N]用来保存最优解的构造过程。M和N分别表示两个序列的长度。该过程的伪代码如下所示:

LCS_LENGTH(X,Y)
    m = length(X);
    n = length(Y);
    for i = 1 to m
      c[i][0] = 0;
    for j=1 to n
      c[0][j] = 0;
    for i=1 to m
       for j=1 to n
           if x[i] = y[j]
              then c[i][j] = c[i-1][j-1]+1;
                   b[i][j] = '';
           else if c[i-1][j] >= c[i][j-1]
                  then c[i][j] = c[i-1][j];
                       b[i][j] = '';
                  else
                       c[i][j] = c[i][j-1];
                       b[i][j] = '';
return c and b

由伪代码可以看出LCS_LENGTH运行时间为O(mn)。

4)构造一个LCS

  根据第三步中保存的表b构建一个LCS序列。从b[m][n]开始,当遇到'\'时,表示xi=yj,是LCS中的一个元素。通过递归即可求出LCS的序列元素。书中给出了伪代码如下所示:

PRINT_LCS(b,X,i,j)
    if i==0 or j==0
        then return
    if b[i][j] == '\'
        then PRINT_LCS(b,X,i-1,j-1)
             print X[i]
     else if b[i][j] == '|'
                then PRINT_LCS(b,X,i-1,j)
             else PRINT_LSC(b,X,i,j-1)

3、编程实现

  现在采用C++语言实现上述过程,例如有两个序列X={A,B,C,B,D,A,B}和Y={B,D,C,A,B,A},求其最长公共子序列Z。完整程序如下所示:

#include<iostream>
#include<string>
#include<cstring>
using namespace std;

#define X_LEN  7
#define Y_LEN  6
string s1="ABCBDAB";
string s2="BDCABA";
int c[X_LEN+1][Y_LEN+1];
char b[X_LEN+1][Y_LEN+1];

void lcs_length();
void print_matrix();
void print_lcs(char b[X_LEN+1][Y_LEN+1],string s,int i,int j);

int main()
{
    memset(b,0,sizeof(b));
    lcs_length();
    print_matrix();
    cout<<"打印子序列:"<<endl;
    print_lcs(b,s1,X_LEN,Y_LEN);
    return 0;
}

void lcs_length()
{
    int i,j;
    for(i=1;i<=X_LEN;i++)
        c[i][0]=0;
    for(i=0;i<=Y_LEN;i++)
        c[0][i]=0;
    for(i=1;i<=X_LEN;i++)
        for(j=1;j<=Y_LEN;j++)
    {
        if(s1[i-1]==s2[j-1])
        {
            c[i][j]=c[i-1][j-1]+1;
            b[i][j]='\\';
        }
        else if(c[i-1][j]>=c[i][j-1])
        {
            c[i][j]=c[i-1][j];
            b[i][j]='|';
        }
        else
        {
            c[i][j]=c[i][j-1];
            b[i][j]='-';
        }
    }
}

void print_matrix()
{
    int i,j;
    cout<<"长度矩阵:"<<endl;
    for(i=0;i<=X_LEN;i++)
    {
        for(j=0;j<=Y_LEN;j++)
            cout<<c[i][j]<<" ";
        cout<<endl;
    }
    cout<<"方向矩阵:"<<endl;
    for(i=0;i<=X_LEN;i++)
    {
        for(j=0;j<=Y_LEN;j++)
            cout<<b[i][j]<<" ";
        cout<<endl;
    }
}

void print_lcs(char b[X_LEN+1][Y_LEN+1],string s,int i,int j)
{
    if(i==0||j==0)
        return;
    if(b[i][j]=='\\')
    {
        print_lcs(b,s,i-1,j-1);
        cout<<s[i-1]<<" ";
    }
    else if(b[i][j]=='|')
    {
        print_lcs(b,s,i-1,j);
    }
    else
        print_lcs(b,s,i,j-1);

}

运行结果:

第十五章 动态规划——最长公共子序列

可以参考:http://www.cnblogs.com/kaituorensheng/archive/2013/03/31/2992319.html

上一篇:Java与C#执行效率之不科学比较


下一篇:C#学习笔记(7)事件