最长公共子序列(LCS)

一、概念

1.给定字符串str = "ABCDADNENXY"

    子序列:从str中任意去掉若干个(含0个)字符,剩下的就是这个str的子序列,如ABC, ABXY, DADXY等,中间不必连续.

    子串:和子序列不同,子串必须是连续的,如ABCD,ENXY,CDADNE都是子串,而AXY不是,因为中间断开了,把连续.

    子串必定是子序列,子序列不一定是子串.

2.最长公共子序列(Longest Common Sequence)

    给出字符串str1和str2,如果一个字符串s同是str1和str2的子序列,则称s为二者的公共子序列,如果s最长,则称s为最长公共子序列,即LCS.

    如str1 = "ABCBDCA", str2 = "DABDA", 二者的一个公共子序列为AA,而最长公共子序列为ABDA.

    再如,str1 = "BACDABC", str2 = "AXBDC", 二者的LCS不止一个,如BDC, ABC, ADC都是LCS,因此需要明确:LCS有时候不唯一.


二、LCS的长度

LCS问题可以分解为,求LCS长度,再求LCS具体是哪个字符串,求LCS长度看起来相对容易一点.

问题描述: A和B分别为长度为x和y的两个字符串,设A = a1, a2, ..., ax, B = b1, b2, ..., by,要求A和B的LCS.

扫描二维码关注公众号,回复: 2191182 查看本文章

思路:观察A和B的最后一个字符,如果相等则该字符一定属于LCS的一部分,否则:该字符有可能属于LCS的一部分.据此构造动态规划的状态转移方程.

具体解法:如果已经给定A和B,我们可以用LCS(x, y)表示A和B的LCS长度,下面我们求递归关系式.

1.

2.如果A和B尾部字符相同

3.如果不相同

4.递推公式的边界

5.完整的递推公式

————————————————————————————————————————————————

最终要求的为LCS(x,y)的值,在已经明确递推关系式的情况下,可以采用递归或"循环打表"的方式用代码求出LCS(x,y)的值.

代码分别如下:

#include<iostream> 
#include<string.h>
using namespace std;

//递归的方法 
//注意 A和B分别为输入的字符串前面加上一个' '空的字符,是为了解决边界问题. 
int LCS(string A, string B, int x, int y)
{
	if(x==0 || y==0)	//边界 
		return 0;
	else				//非边界位置 
	{
		if(A[x] == B[y])	//最后一个字符相同 
			return LCS(A, B, x-1, y-1) + 1;
		else
			return max(LCS(A, B, x, y-1), LCS(A, B, x-1, y));
	}
}

int main()
{
	string A = "BCBACABBACDX";
	string B = "BCXYDADJL";		// A 和 B的LCS为BCAD 
	int x = A.length();
	int y = B.length();
	A = ' ' + A;
	B = ' ' + B;
	int result = LCS(A, B, x, y);
	cout << result << endl;
	
	return 0; 
} 
#include<iostream> 
#include<string.h>
using namespace std;

//循环打表 的方式求LCS[x][y] 
int main()
{	
	string A = "BCBACABBACDX";
	string B = "BCXYDADJL";		// A 和 B的LCS为BCAD 
	int x = A.length();
	int y = B.length();
	A = ' ' + A;
	B = ' ' + B;

	int i, j;
	int ** LCS = new int*[x+1];
	for(i=0;i<=x;i++)
	{
		LCS[i] = new int[y+1];
		for (j=0;j<=y;j++)
			LCS[i][j] = 0;	//LCS[i][j]用来存储 
	}
		
	for(i=0;i<=x;i++)
		for(j=0;j<=y;j++)
			if(i==0 || j==0)
				LCS[i][j] = 0;
			else
			{
				if(A[i] == B[j])
					LCS[i][j] = LCS[i-1][j-1] + 1; 
				else
					LCS[i][j] = max(LCS[i-1][j], LCS[i][j-1]);
			}
	
	int result = LCS[x][y];
	cout << result << endl;
	
	return 0; 
}

三、如何求出具体的LCS

LCS长度问题已经通过递推关系式求解出来了,现在来思考如何求出具体的序列有哪些字符组成. 由于LCS的非唯一, 为了简化问题,我们先求出其中一个,如果有多个LCS不要求都求出来,求出一个即可.

对于两个字符串  A = "ABCBDAB"  和   B = "BDCABA", 由上面的代码已经求出LCS长度为4, 可以看出来LCS可以是BDAB或BCAB或BCBA等,我们的目标是求出其中任意一个.

我们回忆求LCS长度的过程,是打表求出LCS[i][j](0<=i<=x,   0<=j<=y)这个二维数组. 下面手动计算这个过程.

1.二维数组或称为表格LCS[x][y]初始化


2.逐行按照递推关系式运算,结果依次如下:



                    ......

3.最终的表格状态为:


表格右下角的值为LCS[x][y] = 4, 我们需要考虑的是4是怎么计算出来的,反推数字4的来历,记录这个路径,就可以找规律求出其中一个LCS了.

例如,最右下角的4,由于B!=A,因此是取左边4和上面4的最大值,两个4一样大,这个也从侧门反映了这个球LCS的问题的LCS不唯一.

那么我们的代码是怎么计算的呢,看一下代码

if(A[i] == B[j])
	LCS[i][j] = LCS[i-1][j-1] + 1; 
else
	LCS[i][j] = max(LCS[i-1][j], LCS[i][j-1]);

对于(i, j)位置,

如果A[i]==B[j],那么左上方向的数据+1,填到(i, j)位置

否则左边和上边两个方向的最大值,填到(i, j)位置

对于(i, j) = (7, 6),即最右下角,A[7]!=B[6],所以看LCS[6][6]和LCS[7][5],这两个值都是4,怎么办?我们的代码是取

max(LCS[i-1][j], LCS[i][j-1])

那我们怎么记录(7, 6)位置的4是从左边还是从上面来的呢,随便取一个就可以。比如,在遇到max里面两个都相等的时候,取上面的那个,

那么这个4的得到的过程如下图所示:


反推得到的路径为  结束,如果我们遍历一下这个路径(i,j),打印出满足A[i]=B[j]的字符,得到的是:BCBA,这刚好是要求的LCS之一,求出一个即可.

所以,在“循环打表”的代码基础上记录路径就可以得到诸多LCS其中的一个了.

代码如下:

#include<iostream> 
#include<string.h>
using namespace std;

//循环打表 的方式求LCS[x][y] 
int main()
{	
//	string A = "ABCBDAB";
//	string B = "BDCABA";
	string A = "ABCBDABXBXZWUNBV";
	string B = "BDCABAXWBVQUADABA";
	int x = A.length();
	int y = B.length();
	A = ' ' + A;
	B = ' ' + B;

	int i, j;
	int ** LCS = new int*[x+1];
	int ** dir = new int*[x+1];
	//dir用于记录方向 ,规定该数组元素取值只有0 1 2三种,分别表示左上,左,上 
	//dir[i][j]表示LCS[i][j]是从哪个方向得到的 
	for(i=0;i<=x;i++)
	{
		LCS[i] = new int[y+1];
		dir[i] = new int[y+1];
		for (j=0;j<=y;j++)
			LCS[i][j] = 0;	//
	}
	
	for(i=0;i<=x;i++)
		for(j=0;j<=y;j++)
			if(i==0 || j==0)
				LCS[i][j] = 0;
			else
			{
				if(A[i] == B[j])
				{
					LCS[i][j] = LCS[i-1][j-1] + 1; 
					dir[i][j] = 0;	//左上 
				}
				else
				{
					LCS[i][j] = max(LCS[i-1][j], LCS[i][j-1]);
					if(LCS[i-1][j]>=LCS[i][j-1])//如果>=改为>,得到的LCS不一样 
						dir[i][j] = 1;	//i变化  上 
					else
						dir[i][j] = 2;	//j变化  左 
				} 
			}
	
	int result = LCS[x][y];
	cout << result << endl;
	
	//根据dir求出路径,即一系列(i,j), 同时判断 A[i]==B[j]
	i = x, j = y;
	string lcs = "";
	while(!(i==0 || j==0))
	{
		cout << i << " " << j << endl;
		if(A[i]==B[j])
		{
			lcs = A[i] + lcs;
			cout << A[i] << endl; 
		} 
		if(dir[i][j]==0)
			i--, j--;
		else if(dir[i][j]==1)
			i--;
		else
			j--;
	}
	cout << lcs << endl;
	
	return 0; 
}
这样可以求出一个LCS,至于如何求出所有的LCS问题,后续再讨论.


猜你喜欢

转载自blog.csdn.net/ten_sory/article/details/79798064