【动态规划专题】最长公共子序列问题

最长子序列问题(longest -common-subsequence problem)是一个经典的体现动态规划思想的算法问题,问题描述如下:

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

对于该问题,倘若我们使用暴力搜索法进行求解,无疑就需要穷举X的所有子序列。然后对每一个子序列检查它是否也是Y的子序列,记录找到最长的子序列。X的每一个子序列对应的X的下标集合为{1,2,3,…m}的一个子集,所以X有pow(2,m)个子序列,因此暴力搜索的运行时间为指数阶,对于较长的序列无疑是不实用的。所以,对于该问题的求解我们应该另辟他径。 那么,我们可不可以采用动态规划思想来求解此类问题呢? 我们先来复习一下使用动态规划所要满足的条件。
1.最优化原理(最优子结构性质) 最优化原理可这样阐述:一个最优化策略具有这样的性质,不论过去状态和决策如何,对前面的决策所形成的状态而言,余下的诸决策必须构成最优策略。简而言之,一个最优化策略的子策略总是最优的。一个问题满足最优化原理又称其具有最优子结构性质。
2.无后效性将各阶段按照一定的次序排列好之后,对于某个给定的阶段状态,它以前各阶段的状态无法直接影响它未来的决策,而只能通过当前的这个状态。换句话说,每个状态都是过去历史的一个完整总结。这就是无后向性,又称为无后效性。
3.子问题的重叠性 动态规划将原来具有指数级时间复杂度的搜索算法改进成了具有多项式时间复杂度的算法。其中的关键在于解决冗余,这是动态规划算法的根本目的。动态规划实质上是一种以空间换时间的技术,它在实现的过程中,不得不存储产生过程中的各种状态,所以它的空间复杂度要大于其它的算法。

对于第一个条件,我们先来分析一下该问题。 我们可以先假设Zk={z1,z2,z3,…zk}是Xm={x1,x2,x3,…xm}和Yn={y1,y2,y3,…yn}的最长公共子序列。那么可以分为三种情况讨论。
(1)当xm=yn=zk,则Z(k-1)={z1,z2,z3,…z(k-1)}是X(m-1)和Y(n-1)的最长公共子序列
证明:如果Z(K-1)={z1,z2,z3,…z(k-1)}不是X(m-1)和Y(n-1)的最长公共子序列,那么它们一定存在一个最长公共子序列。设M为X(m-1)和Y(n-1)的最长公共子序列,M的长度大于Z(k-1)的长度,即|M|>|Z(K-1)|。如果在X(m-1)和Y(n-1)的后面添加一个相同的字符xm=yn,则zk=xm=yn,
|M+{zk}|>|Z(k-1)+{zk}|=|Zk|,那么Zk不是Xm和Yn的最长公共子序列,这与我们的假设相互矛盾,故(1)定理得证!
(2)当xm<>yn,xm<>zk时,则Zk={z1,z2,z3,…zk}是X(m-1)和Yn的最长公共子序列

证明:如果Z(K-1)={z1,z2,z3,…z(k-1)}不是X(m-1)和Yn的最长公共子序列,那么它们一定存在一个最长公共子序列。设M为X(m-1)和Yn的最长公共子序列,M的长度大于Zk的长度,即|M|>|Zk|。如果在X(m-1)的后面添加一个相同的字符xm,那么M也是Xm和Yn的最长公共子序列,又因为|M|>|Zk|,那么Zk不是Xm和Yn的最长公共子序列,这与我们的假设相互矛盾,故(2)定理得证!
(3)当xm<>yn,yn<>zk时,则Zk={z1,z2,z3,…zk}是X(m)和Y(n-1)的最长公共子序列。
证明:如果Z(K-1)={z1,z2,z3,…z(k-1)}不是X(m)和Y(n-1)的最长公共子序列,那么它们一定存在一个最长公共子序列。设M为X(m)和Y(n-1)的最长公共子序列,M的长度大于Zk的长度,即|M|>|Zk|。如果在Y(n-1)的后面添加一个相同的字符yn,那么M也是Xm和Yn的最长公共子序列,又因为|M|>|Zk|,那么Zk不是Xm和Yn的最长公共子序列,这与我们的假设相互矛盾,故(3)定理得证!
综上所述,我们可以推断出LCS问题具有最优化子结构性质。
而对于第二个条件,我们先来理解一下什么是无后效性。 无后效性是指如果在某个阶段上过程的状态已知,则从此阶段以后过程的发展变化仅与此阶段的状态有关,而与过程在此阶段以前的阶段所经历过的状态无关。即某阶段的状态一旦确定,则此后过程的演变不再受此前各状态及决策的影响。也就是说,“未来与过去无关”,当前的状态是此前历史的一个完整总结,此前的历史只能通过当前的状态去影响过程未来的演变。具体地说,如果一个问题被划分各个阶段之后,阶段k中的状态只能通过阶段k+1中的状态通过状态转移方程得来,与其他状态没有关系,特别是与未发生的状态没有关系,这就是无后效性。
显而易见,LSC问题是符合无后效性这个条件的。 最后,对于最后一个条件 由上面的(1)(2)(3)定理我们知道,在求X={x1,x2,x3,…xm}和Y={y1,y2,y3,..yn}的一个LCS时,我们需要求解一个或两个子问题。如果xm=yn,我们应该求解X(m-1)和Y(n-1)的一个LCS。将xm=yn追加到这个LSC的末尾,就可以得到X和Y的一个LSC。如果xm<>yn,我们必须求解两个子问题:求X(m-1)和Yn的一个LSC与Xm和Y(n-1)的一个LCS。两个LCS较长者即为X和Y的一个LCS。由于这些情况覆蓋了所有可能性,因此我们知道必然有一个子问题的最优解出现在X和Y的LCS中。
我们可以轻易地看出LCS问题的重叠子问题性质,为了求X和Y的一个LCS,我们可能需要求解X和Y(n-1)的一个LCS以及X(m-1)和Y的一个LCS。但是这几个子问题都包括求解X(m-1)和Y(n-1)的LCS的子子问题。 根据LCS的问题的最优子结构性质,可以得到下面的状态转移公式:

#include <iostream>
#include <string>
using namespace std;
const int N=1002;
int dp[N][N];

void LCSL(string s1,string s2)
{
	int i,j;
	for(i=1;i<=s1.length();i++)
	{
		for(j=1;j<=s2.length();j++)
		{
			if(s1[i-1]==s2[j-1])//如果当前字符相同,则公共子序列的长度为该字符前的最长公共子序列加1
			{
				dp[i][j]=dp[i-1][j-1]+1;
			}
			else
			{
				if(dp[i][j-1]>=dp[i-1][j])
				{
					dp[i][j]=dp[i][j-1];
				}
				else
				{
					dp[i][j]=dp[i-1][j];
				}
			 } 
		}
	}
}
//根据记录下来的信息构造最长公共子序列 
void print(string s,int i,int j)
{
	if(i==0||j==0)
	{
		return;
	}
	if(dp[i][j]==dp[i][j-1])
	{
		print(s,i,j-1);
	}
	else if(dp[i][j]==dp[i-1][j])
	{
		print(s,i-1,j);
	}
	else if(dp[i][j]==dp[i-1][j-1]+1)
	{
		print(s,i-1,j-1);
		cout<<s[i-1];
	} 
}

int main()
{
	string s1,s2;
	cout<<"请输入字符串s1:"<<endl;
	cin>>s1;
	cout<<"请输入字符串s2:"<<endl;
	cin>>s2;
	for(int i=0;i<s1.length();i++)
	{
		dp[i][0]=0;
	}
	for(int j=0;j<s2.length();j++)
	{
		dp[0][j]=0;
	}
	LCSL(s1,s2);
	cout<<"s1和s2的最长公共子序列长度是:"<<dp[s1.length()][s2.length()]<<endl;
	cout<<"s1和s2的最长公共子序列是:";
	print(s1,s1.length(),s2.length());
	return 0;
 } 

点赞