动态规划入门之最长公共子序列(LCS)

LCS是动态规划在字符串问题中应用的典型。问题描述:给定2个序列,求这两个序列的最长公共子序列,不要求子序列连续。例如{2,4,3,1,2,1}和{1,2,3,2,4,1,2}的结果是{2,3,2,1}或者{2,4,1,2}。

思路:如果不用动态规划去做,而用暴力法,则必须找出其中一个序列的所有子序列(LIS如果用暴力法思路也是如此),然后判断这个子序列是不是另外一个序列的子序列。判断一个序列是不是另一个序列的子序列可以在O(n)内解决(只需要两个指针,然后依次比较并移动),但是怎么去找一个序列的所有子序列呢,就是就这个集合的所有子集,这是指数级别的复杂度。如果用动态规划去做呢?我们可以用d[i][j]表示序列a[0~i]和序列b[0~j]的最长公共子序列的长度,这是状态;那么状态转移方程:

d[i][j]=d[i-1][j-1]+1 if a[i]=b[j]; and  d[i][j]=max{d[i][j-1],d[i-1][j]} if a[i] != b[j]。也就是说,如果i和j上的元素相等,那么d[i-1][j-1]+1就是d[i][j],否则,就要看d[i-1][j]和d[i][j-1]谁大了。在这里面,状态用一个二维数组表示,也就是一个矩阵。我这里引用别人的一张图帮助理解:

《动态规划入门之最长公共子序列(LCS)》

上图中序列分别为{B,D,C,A,B,A}和{A,B,C,B,D,A,B}。里面包含了所有的状态。从上图可以知道,每个状态都可以从它的左上、左、或者上方走过来。具体的条件就是上面说的a[i]是否和b[j]相等。我们如果要求出最终的最长公共子序列,就需要得到上面的状态图,然后从图的右下角,根据路径回溯到左上角就行了。

接下来给出JAVA实现的LCS代码:

/**
 * 
 * @author kerry
 * 求两个序列的最长公共子序列
 */
public class LCS {

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		int[] a={2,4,3,1,2,1};
		int[] b={1,2,3,2,4,1,2};
		int out=lcsSolution(a,b);
		System.out.println(out);
		
	}
	/*计算状态矩阵:复杂度为O(n)*/
	public static int lcsSolution(int[] a,int[] b){
		int len1=a.length;
		int len2=b.length;
		if(len1==0||len2==0)return 0;
		int[][] status=new int[len1+1][len2+1];
		for(int i=0;i<=len1;i++){
			for(int j=0;j<=len2;j++){
				status[i][j]=0;
			}
		}
		int[][] path=new int[len1][len2];
		//status[i][j]表示0~i和0~j这两个序列的最长公共子序列的长度,也就是状态
		for(int i=0;i<=len1;i++){
			for(int j=0;j<=len2;j++){
				if(i==0||j==0)status[i][j]=0;
				//下面是状态转移
				else if(a[i-1]==b[j-1]){
					status[i][j]=status[i-1][j-1]+1;//斜方向
					path[i-1][j-1]=1;
				}
				else {
					if(status[i][j-1]>status[i-1][j]){
						status[i][j]=status[i][j-1];//左方向
						path[i-1][j-1]=2;
					}
					else{
						status[i][j]=status[i-1][j];//上方向
						path[i-1][j-1]=3;
					}
				}
			}
		}
		printAns(path,a);
		return status[len1][len2];
	}
	//打印结果,这里打印的结果是反的,如果要想正向打印,可以利用递归的方法,类似于从尾到头打印单链表的做法,也可以用栈
	public static void printAns(int[][] path,int[] a){
		int len1=path.length;
		int len2=path[0].length;
		int i=len1-1;
		int j=len2-1;
		while(j>=0&&i>=0){
			if(path[i][j]==1){
				System.out.print(a[i]+"->");
				i--;j--;
			}
			else if(path[i][j]==2){
				j--;
			}
			else i--;
		}
		System.out.println();
	}
}

为了能打印出LCS,需要继续每次状态转移时候选择的路径,代码里用path这个二维数组记录,最后根据这个记录打印结果,这里打印的结果是反的,要想正向打印可以参照代码中说的方法。另外:左和上这两个方向存在优先级关系,优先级不同,打印出来的结果可能不同,但都是问题的解(解不唯一)。

假设序列长度分别为m和n,那么计算矩阵的复杂度为O(m*n),打印的复杂度为 O(m+n)。

如有错误,请多多指出~

    原文作者:有点傻的余
    原文地址: https://blog.csdn.net/kerryfish/article/details/25649589
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞