关于动态规划解决最长公共子序列

关于动态规划解决最长公共子序列

最长公共子序列问题是动态规划的经典问题之一,描述如下:

给定两个序列X和Y,当另一序列Z既是X的子序列又是Y的子序列,则称Z是X和Y的公共子序列,求最长的公共子序列Z。

动态规划第一步:构造具有最优子结构的解结构
设X={x1,x2,x3,…,xm},Y={y1,y2,y3,…,yn}的最长公共子序列为Z={z1,z2,z3,…,zk}。

那么由问题的描述可知X,Y,Z有如下性质:

①若xm=yn,则zk=xm=yn,那么Zk-1是Xm-1和Yn-1的最长公共子序列。

(①的思路:很显然,当X和Y的最后一个值相等时,无论前面的公共子序列是什么,公共子序列的最后一个值就是xm的值)

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

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

(②和③的思路:显然由①反推可知,如果X与Y最后一个子元素不同,那么X和Y的最后一位至少有一个不是公共子序列的最后一位,如果X的最后一位不是公共子序列的最后一位,那么对于本题需要求的公共子序列来说X与Xm-1没有任何区别,可以将X最后一位删去,如果Y的最后一位不是公共子序列的最后一位,那么删去Y的最后一位也不会影响公共子序列。)

至此,第一步完成。

动态规划第二步:根据最优子结构的性质得出递推式

递归式已经隐含于①所构造出来的最优子结构了。
用c[i][j]记录序列Xi和Yj的最长公共子序列的长度,其中
Xi={x1,x2,x3,…,xi};Yj={y1,y2,y3,…,yj}
那么由前述的三种情况可以推知:

①当i=0 或 j=0时,c[i][j]=0;

解释:如果有一个序列是空序列,自然最长公共子序列长度为0。

②当i,j>0;xi=yj时,c[i][j]=c[i-1][j-1]+1

解释:这一条是由①性质得出的,因为Zk-1是Xm-1与Yn-1的最长公共子序列,那么Zk的长度自然是Xm-1与Yn-1的最长公共子序列长度+1。

③当i,j>0;xi≠yj时,c[i][j]=max{c[i][j-1],c[i-1][j]}

解释:这一条是由②和③性质共同得出的,因为如果X与Y最后一个子元素不同,那么X和Y的最后一位至少有一个不是公共子序列的最后一位。也就是X序列与Y序列至少有一个能删去最后一位。但由于计算c[i][j]时,我们无法得知公共序列Z的最后一位zk是什么,无法拿去与xi,yj比较,所以无法得知应该删去X序列的最后一位(c[i][j]=c[i-1][j])还是删去Y序列的最后一位(c[i][j]=c[i][j-1]),只能将两种情况都计算出来,再比较他们的大小,留下序列最长的放入c[i][j]。

至此,第二步完成。

动态规划第三步:根据递归关系,设计算法计算最优值。

计算最优值的算法实际上就是计算c[i][j],写代码前先整理下c[i][j]的计算原理:
由第二步得出的c[i][j]的分段函数可知:若将c[i][j]看做二维矩阵,c[i][j]的计算总是依赖与其左(c[i][j-1])、上(c[i-1][j])、左上(c[i-1][j-1])这三个坐标的值,所以计算的顺序应当是自左向右,自上而下。
在网上找到的一幅图:
在这里插入图片描述
这幅图的意思是:xi序列为{A,B,C,B,D,A,B},yj序列为{B,D,C,A,B,A}。
由于左边边界和上方边界没有左(c[i][j-1])、上(c[i-1][j])、左上(c[i-1][j-1])这三个坐标,所以额外设置了一个第0行与第0列,全部都置为了0,方便算法计算。

先以c[4][3]的计算为例,比较x4和y3,由于x4=B,y3=C,x4≠y3,所以属于第三个递推式c[i][j]=max{c[i][j-1],c[i-1][j]},求其左方坐标的值c[i][j-1]和上方坐标的值c[i-1][j],比较取最大者,由于c[4][2]=1,c[3][3]=2,所以c[4][3]=c[3][3]=2。
再以c[4][5]的计算为例,比较x4和y5,由于x4=B,y5=B,x4=y5,所以属于第二个递推式c[i][j]=c[i-1][j-1]+1。即其左上角坐标的值+1,c[4][5]=c[3][4]+1=2+1=3。

代码如下:

//m是X序列的长度,n是Y序列的长度,ps而实际上因为多增加了一行一列,表示它们的数组的长度增加了1
//x是X序列(数组)的首地址,y是Y序列(数组)的首地址
//c是上文提到的记录最长公共子序列长度的二维数组c[][]
//b是用于第四步构造最优解的一个二维矩阵,第四步时再说。
	void LCSLength(int m,int n,char *x,char *y,int **c,int **b){
		int i,j;
		//下面的两个循环就是把额外加的第0行与第0列置0,防止左边界和上边界越界
		for(i=0;i<=m;i++)
			c[i][0] = 0;
		for(i=1;i<=n;i++)
			c[0][j] = 0;
		//下面的双重循环就是自左向右,自上而下的开始计算c[][]
		for(i=1;i<=m;i++){
			for(j=1;j<=m;j++){
				if(x[i]==y[j]){
					c[i][j]=c[i-1][j-1]+1;
					b[i][j]=1;
				}
				//若x[i]≠y[j],则计算左边和上面坐标的值,取最大者
				else if(c[i-1][j]>=c[i][j-1]){
					c[i][j]=c[i-1][j];
					b[i][j]=2;
				}
				else {
					c[i][j]=c[i][j-1];
					b[i][j]=3;
				}
			}
		}
	}

相信理解了上面的解说之后,代码肯定很简单了。

动态规划第四步:构造最优解,根据算法获取的信息构造出最长公共子序列。
根据算法得到表示最长公共子序列的二维矩阵c[m][n]之后,我们已经知道,Xm与Yn的最长公共子序列的长度就是c[m][n],也就是矩阵中最右下角、最后一个数值。但c[m][n]只是最长公共子序列的长度而已,而我们需要的是最长公共子序列Zk,很明显的一点就是,必须要从c[m][n]开始回溯我们才知道最优子序列是什么,但很明显,回溯是有歧义的,有时向数个方向回溯都能满足要求,必须消除这种歧义。

而这就是代码中b[m][n]矩阵的意义,由于c[i][j]的计算只能从三个方向左(c[i][j-1])、上(c[i-1][j])、左上(c[i-1][j-1]),那么只需要设置一个b[i][j]记录下c[i][j]是从哪个方向上获取值的,我们就能没有歧义的从c[m][n]回溯回起点,从而找出Zk序列。可以规定:b[i][j]=1,意味着c[i][j]的值是从左上c[i-1][j-1]+1而来,b[i][j]=2,意味着c[i][j]的值是从上面c[i-1][j]而来,b[i][j]=3,意味着c[i][j]的值是从左边c[i][j-1]而来。

在这里插入图片描述
敏锐一点应该就能发现了,b[m][n]的作用其实就是图中的箭头,箭头所指就是回溯的路径,回溯路径中任一一个b[i][j],只要满足x[i]=y[j](字符相等),就意味着它是最长公共子序列中的一个,回溯会直到左边界和上边界为止。(因为左边界和上边界是c[i][j]=0,这意味着最长公共子序列长度为0。)

原理清楚后,上代码:

	void LCS(int i,int j,char *x,int **b){
		//下面是递归的出口,当i或j=0时,c[i][j]==0,所以没有公共子序列了。
		if(i==0||j==0)
			return;
		//b[i][j]==1时,x[i]==y[j],这是赋值b的时候就确定的事情,所以满足前面总结的规律。
		if(b[i][j]==1){
			LCS(i-1,j-1,x,b);
			cout<<x[i];
		}
		//如果b[i][j]==2,那么说明x[i]≠y[j],自然也就不属于公共子序列,然后再继续回溯,下面一个也是同理。
		else if(b[i][j]==2)
			LCS(i-1,j,x,b);
		else
			LCS(i,j-1,x,b);
	}
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值