动态规划-求最长公共子序列LCS长度-输出所有的最长公共子序列LCS-Java实现

题目:
给定两个序列X={x1,x2,…,xm}和Y={y1,y2,…,yn},求X和Y的最长公共子序列。

1 最长公共子序列概念

实现明确一个点就是,子序列是一个序列中去掉若干元素后得到的序列,也就是说子序列的元素下标是递增的就行,不需要在原序列中连续。

最长公共子序列,显而易见就是序列X和Y都有的最长的子序列。

2 BruteForce 暴力求解法

对于序列X的所有长度不超过Y的子序列,都检查是否也是Y的子序列,在检查过程中记录长度做长的。

假设X长度小于Y,X有m个元素,每个元素都有2种选择,在或不在,所以X一共有2m个不同子序列。此方法需要指数时间。

3 DP 动态规划法

① 最优子结构

首先我们来检查下LCS问题能不能用DP来做。

DP的问题,需要分成的子问题具有最优子结构性质(问题的最优解中包含着每个子问题的最优解),子问题高度重复性,(当然不用把这个理解成必须,只是说这样的问题比较适合用DP来做)。

假设X和Y的最长公共子序列为Z={z1,z2,…,zk},
则把LCS问题,分成下面这样的几个子问题:
(1)若xm=yn,则zk=xm=yn,且Zk-1是Xm-1和Yn-1的最长公共子序列。
(2)若xm≠yn,且zk≠xm,则Zk是Xm-1和Yn的最长公共子序列。
(2)若xm≠yn,且zk≠yn,则Zk是Xm和Yn-1的最长公共子序列。

现在来证明一下,以上的子问题具有最优子结构性质,用反证法,
(1) 若zk不等于xm,因为xm=yn,所以把xm加在zk后面,{z1,z2,…,zk,xm}是X和Y的长度为k+1的公共子序列,这与Z是X和Y的最长公共子序列矛盾,因此,必有zk=xm=yn,Zk-1
  若Zk-1不是Xm-1和Yn-1的最长公共子序列,则Xm-1和Yn-1存在长度比k-1大的公共子序列,则将xm加在其后面,就产生了一个X和Y的长度比k大的公共子序列了,这与这与Zk是X和Y的最长公共子序列矛盾,
  综上两点,故zk=xm=yn,且Zk-1是Xm-1和Yn-1的最长公共子序列。

(2)若Zk不是Xm-1和Yn的最长公共子序列,则Xm-1和Yn还存在更长的最长公共子序列W,则W也是X和Y的公共子序列,且W长度>k,这与Z(长度为k)是X和Y的最长公共子序列矛盾。故Zk是Xm-1和Yn的最长公共子序列。
(3)若Zk不是Xm和Yn-1的最长公共子序列,则Xm和Yn-1还存在更长的最长公共子序列W,则W也是X和Y的公共子序列,且W长度>k,这与Z(长度为k)是X和Y的最长公共子序列矛盾。故Zk是Xm和Yn-1的最长公共子序列。
综上三点,两个序列最长公共子序列包含了这两个序列的前缀的最长公共子序列,(这句话有点拗口,其实就是X和Y的LCS包含了X1≤r≤m和Y1≤s≤n的LCS)因此最长公共子序列具有最优子结构性质。

② 子问题的递归结构
用C[i][j]记录序列Xi和Yj的LCS长度。

动态规划-求最长公共子序列LCS长度-输出所有的最长公共子序列LCS-Java实现_第1张图片

③求最优值,输出LCS
为了构造出LCS,引入一个二维数组b[m][n],用来记录c[i][j]的值是由哪个子问题得到的。
(1)b[i][j]=1,则表示Xi和Yj的LCS是从Xi-1和Yj-1的LCS得到的,在Xi-1和Yj-1的LCS后加上一个xi得到。
(2)b[i][j]=2,则表示Xi和Yj的LCS是从Xi-1和Yj的LCS得到的,就是Xi-1和Yj的LCS。
(3)b[i][j]=3,则表示Xi和Yj的LCS是从Xi和Yj-1的LCS得到的,就是Xi和Yj-1的LCS。
(4)b[i][j]=4,则表示Xi和Yj的LCS是从Xi-1和Yj的LCS,和Xi和Yj-1的LCS得到的,此时情况表示的是,C[i-1,j]=C[i,j-1]的情况,这时候有两个元素可能不同的,但是长度相同的LCS,为了不遗漏,所以要两个方向进行搜索。

注意,一般资料上都是分成(1),(2),(3)这三种情况,将(4)并入(2)中,这样会导致算法不能搜索到所有的LCS,(当然最长长度可以正确得出,只是LCS往往不止一个,得不到所有的LCS情况),但是加上(4)后,会导致重复输出的问题,比如BA和BC会输出两个B。但是如果没有(4),如BED和BDE会只输出BE或者BD。
-----(结合代码看更容易理解)----

4 java代码实现
注意几个点:
(1)数组C的大小应该是[X.length+1][Y.length+1],要存储子序列长度为0的情况。C[1][2]表示,X的第一个元素和Y的前两个元素的LCS长度,C[i][0]和C[0][j]都是0,注意下碰到X和Y不要数组越界了。
(2)增加了状态(4)后,会导致出现分支:

private static void printLCS(char[]X,char[]Y,int i,int j,int[][]b)
{
	if(i==0||j==0) return;//递归出口
    if(b[i][j]==1)
			{
		          printLCS(X,Y,i-1,j-1,b);
		          System.out.print(X[i-1]);//因为是递归,故X[i-1]输出的时候正好是它本来在的位置,不会逆序	 
			}
	else if(b[i][j]==2)
		printLCS(X,Y,i-1,j,b);
	else if(b[i][j]==3)
		printLCS(X,Y,i,j-1,b);
	else 
		{
		   printLCS(X,Y,i,j-1,b);
		   System.out.print('/');
		   printLCS(X,Y,i-1,j,b);
		}
	
}

但是这样输出在某些情况下不会输出正确的结果,如ABCBDAB和BDCABA会输出:BD/BCAB/BCBA,而正确输出应该是BDAB/BCAB/BCBA,看下递归过程:
动态规划-求最长公共子序列LCS长度-输出所有的最长公共子序列LCS-Java实现_第2张图片
也就说递归时候先到第一个分支b(4,3)输出了BC,接着到另一个分支b(5,2)输出了BD,最后再return一起输出AB,所以导致了输出BDAB/BCAB,变成了BD/BCAB。
解决方法:
不能利用递归的特点输出序列了,只能用一个字符串把结果都保存下来,最后逆序输出。(如果不要求输出全部的LCS,可以按上述方法做,并且不用加b[][]的第四个状态)
输出修改如下:

private static void printLCS(char[]X,char[]Y,int i,int j,int[][]b,String s)
{
	if(i==0||j==0) //递归出口
	{
        StringBuilder s2=new StringBuilder(s);
        System.out.println(s2.reverse());
        return;
	}
    if(b[i][j]==1)
			{
                   s=s+X[i-1];
		          printLCS(X,Y,i-1,j-1,b,s);
			}
	else if(b[i][j]==2)
		printLCS(X,Y,i-1,j,b,s);
	else if(b[i][j]==3)
		printLCS(X,Y,i,j-1,b,s);
	else 
		{
		   printLCS(X,Y,i,j-1,b,s);
		   printLCS(X,Y,i-1,j,b,s);
		}
}

这里有个很巧妙的一点就是,用String作为参数,String作为参数传值,是不会改变String本身的值的,虽然String是引用数据类型,但是String是不可变的,再利用StringBuilder的倒置函数,可以直接输出结果。如果你把StringBuilder作为参数,StringBuilder的对象是能修改的,比如说BDAB和BCAB,从AB开始产生分支,第一个分支得到BDAB,但是s也被修改成了BDAB,这时候再进行第二个分支,就会输出BCBDAB了。当然如果是C++,直接传就行,没有都是传引用的烦恼。

整体代码如下:
源代码

5 算法优化
其实这个算法里,可以不用b[][]数组,输出时候,可以直接用C[][]数组自身进行比较,来确定C[][]的值是由哪个值所确定的。不想写了,网上找了个实现:代码
可以参考一下。

你可能感兴趣的:(算法,java,LCS,动态规划,Java,最长公共子序列)