3.3 最长公共子序列

      1、问题的理解与描述

一个给定序列的子序列是在该序列中删去若干元素后得到的序列。确切地说,若给定序列X= { x1, x2,…, xm},则另一序列Z= {z1, z2,…, zk}X的子序列是指存在一个严格递增的下标序列 {i1, i2,…, ik},使得对于所有j=1,2,…,kZj=Xij。例如,序列Z={B,C,D,B}是序列X={A,B,C,B,D,A,B}的子序列,相应的递增下标序列为{2,3,5,7}

      给定两个序列XY,当另一序列Z既是X的子序列又是Y的子序列时,称Z是序列XY公共子序列。例如,若X= { A, B, C, B, D, A, B}Y= {B, D, C, A, B, A},则序列{B,C,A}XY的一个公共子序列,序列{B,C,B,A}也是XY的一个公共子序列。而且,后者是XY的一个最长公共子序列,因为XY没有长度大于4的公共子序列。

      最长公共子序列问题:给定两个序列X= {x1, x2,…, xm}Y= {y1, y2, … , yn},要求找出XY的一个最长公共子序列。

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

      穷举搜索法最容易想到的方法。对X的所有子序列,检查它是否也是Y的子序列,从而确定它是否为XY的公共子序列。并且在检查的过程中记录最长的公共子序列。X的所有子序列都检查过后即可求出XY的最长公共子序列。X的每个子序列相应于下标集{1,2,...,m}的一个子集。因此共有2^m个不同子序列,因此,穷举法需要指数级别的运算时间。

      事实上,最长子序列问题具有最优子结构的性质。

      设序列X={x1, x2,…, xm}Y={y1, y2, … , yn}的最长公共子序列为Z={z1, z2,…, zk}。则有:

       (1)xm=yn,zk=xm=yn,zk-1xm-1yn-1的最长公共子序列。

       (2)xmynzkxm,则Z是xm-1Y的最长公共子序列。

       (3)xmynzk≠yn,则ZXYn-1的最长公共子序列。

       其中,Xm-1={x1, x2,…, xm-1}Yn-1={y1, y2, … , yn-1}Zk-1={z1, z2,…, zk-1}

3、子问题的递归结构

最长公共子序列的最优子结构性质可知,要找出X={x1, x2,…, xm}和Y={y1, y2, … , yn}的最长公共子序列,可以按以下方式递归进行:当xm=yn时,找出Xm-1Yn-1的最长公共子序列,然后在尾部加上xm(=yn)即可得X和Y的最长公共子序列。当xmyn时,必须解两个子问题,即找出Xm-1和Y的一个最长公共子序列及X和Yn-1的一个最长公共子序列。这两个公共子序列中较长者为X和Y的最长公共子序列。
由此递归结构容易看到最长公共子序列问题具有子问题重叠性质。例如,在计算X和Y的最长公共子序列时,可能要计算X和Yn-1及Xm-1和Y的最长公共子序列。而这两个子问题都包含一个公共子问题,即计算Xm-1Yn-1的最长公共子序列。
首先建立子问题最优解的递归关系。 用c[i][j]记录序列 Xi Yj 的最长公共子序列的长度。其中, Xi ={ x 1 , x 2 ,…, x i }, Yj ={ y 1 , y 2 , … , y j }。当i=0或j=0时,空序列是 Xi Yj 的最长公共子序列。故此时,c[i][j]=0 ;当i,j>0,xi=yj时,c[i][j]=c[i-1][j-1]+1;当i,j>0,xiyj时, c[i][j]=max{c[i][j-1],c[i-1][j]},由此建立递推关系如下:

4、计算最优值

计算最长公共子序列长度的动态规划算法lcsLength以序列X={x1, x2,…, xm}Y={y1, y2, … , yn}作为输入。输出两个数组c和b。其中c[i][j]存储XiYj最长公共子序列的长度,b[i][j]记录c[i][j]的值是由哪一个子问题的解得到的,这在构造最长公共子序列时要用到。问题的最优值,即X和Y的最长公共子序列的长度记录于c[m][n]中。
算法的伪代码描述:
<span style="font-size:24px;">LCS-LENGTH(X, Y, b)
1  m <-- length(X)
2  n <-- length(Y)
3  创建数组c
4  for  i <-- 1 to m
5      do c[i][0] <-- 0
6  for  j <-- 0 to n
7      do c[0][j] <-- 0
8  for i <-- 1 to m
9      do for  j <-- 1 to n
10            do if xi=yj
11                    then c[i][j]=c[i-1][j-1]+1
12                         b[i][j] = 1
13                    else if c[i-1][j]>=c[i][j-1]
14                                then c[i][j]=c[i-1][j]
15                                     b[i][j] = 2
16                                else c[i][j]=c[i][j-1]
17                                     b[i][j] = 3
18  return c</span>


算法的实现:
       /** 
	* @MethodName:lcsLength 
	* @Description: 构造c[m][n]
	* @param x 序列X
	* @param y 序列Y
	* @param b 记录c[i][j]是由哪一个问题得到的,用于构造最优解
	* @return
	*/
	public static int[][] lcsLength(char []x, char []y, int[][]b){
		int m=x.length-1;//序列X长度
		int n=y.length-1;//序列Y长度
		int [][]c = new int[m+1][n+1];//创建数组c,用于保存序列Xi和Yj的最长公共子序列的长度
		//当xi=0或yj=0时
		for(int i=1; i<=m; i++){
			c[i][0]=0;
		}
		for(int j=0; j<=n; j++){
			c[0][j]=0;
		}
		//计算序列Xi和Yj的最长公共子序列的长度
		for(int i=1; i<=m; i++){
			for(int j=1; j<=n; j++){
				if(x[i]==y[j]){//xi=yj
					c[i][j]=c[i-1][j-1] + 1;
					b[i][j] = 1;
				}
				//xi!=yj
				else if (c[i-1][j]>=c[i][j-1]){
					//序列Xi-1和Yj的最长公共子序列的长度大于等于序列Xi和Yj-1的最长公共子序列的长度
					c[i][j]=c[i-1][j];
					b[i][j] = 2;
				}else{
					//序列Xi-1和Yj的最长公共子序列的长度小于序列Xi和Yj-1的最长公共子序列的长度
					c[i][j]=c[i][j-1];
					b[i][j] = 3;
				}
			}
		}
		return c;
	}

5、构造最优解

由算法lcsLength计算得到的数组b可用于快速构造序列 X={ x 1 , x 2 ,…, x m } Y={ y 1 , y 2 , … , y n } 的最长公共子序列。从b[m][n]开始,依其值在数组b中搜索,当b[i][j]=1时,表示Xi和Yj的最长公共子序列是由Xi-1和Yj-1的最长公共子序列在尾部加上xi所得到的子序列。当b[i][j]=2时,表示Xi和Yj的最长公共子序列与Xi-1和Yj的最长公共子序列相同。当b[i][j]=3时,表示Xi和Yj的最长公共子序列与Xi和Yj-1的最长公共子序列相同。
下面的算法lcs实现根据b的内容打印出 X i 和Y j 的最长公共子序列。通过算法调用lcs(m,n,x,b)便可打印出序列X和Y的最长公共子序列。
       /** 
	* @MethodName:lcs 
	* @Description: 根据b的内容打印出Xi和Yj的最长公共子序列
	* @param i 序列X的长度
	* @param j 序列Y的长度
	* @param x 序列X
	* @param b 记录c[i][j]是由哪一个问题得到的数组
	*/
	public static void lcs(int i, int j, char []x, int [][]b){
		if(i==0 ||j==0) return;
		if(b[i][j]==1){
			lcs(i-1, j-1, x, b);
			System.out.print(x[i]);
		}else if(b[i][j]==2){
			lcs(i-1, j, x, b);
		}else{
			lcs(i, j-1, x, b);
		}
	}
在算法lcs中,每一次递归调用使i或j减1,因此算法的计算时间为O(m+n)。

6、算法的改进

对于一个具体问题,按照一般的算法设计策略设计出的算法,往往在算法的时间和空间需求上还有较大的改进余地。通常可以利用具体问题的一些特殊性对算法做进一步的改进。例如,在算法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]究竟由哪个值确定。对于给定的数组元素c[i][j],可以不借助于数组b而仅借助于数组c本身,在O(1)时间内确定c[i][j]的值是由c[i-1][j-1],c[i-1][j]和c[i][j-1]中哪一个数值元素所确定的。
既然b对于算法LCS不是必要的,那么算法LCS_length便不必保存它。这一来,可节省θ(mn)的空间,而LCS_length和LCS所需要的时间分别仍然是Ο(mn)和Ο(m+n)。
另外,如果只需要计算最长公共子序列的长度,则算法的空间需求还可大大减少。事实上,在计算c[i][j]时,只用到数组c的第i行和第i-1行。因此,只要用2行的数组空间就可以计算出最长公共子序列的长度。更进一步的分析还可将空间需求减至O(min(m, n))。
            <span style="font-size:24px;">/** 
	* @MethodName:lcsEx 
	* @Description: 改进后的lcs
	* @param i 序列X的长度
	* @param j 序列Y的长度
	* @param x 序列X
	* @param y 序列Y
	* @param c 序列Xi和Yj的最长公共子序列的长度数组
	*/
	public static void lcsEx(int i, int j, char []x, char []y, int [][]c){
		if(i==0 ||j==0) return;
		if(x[i] == y[j]){
			lcsEx(i-1, j-1, x, y, c);
			System.out.print(x[i]);
		}else if(c[i-1][j]>=c[i][j-1]){
			lcsEx(i-1, j, x, y, c);
		}else{
			lcsEx(i, j-1, x, y, c);
		}
	}</span>

例如:对X=<A,B,C,B,D,A,B>和Y=<B,D,C,A,B,A>算法产生的表c以及回溯构造最优解的过程如下:

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值