动态规划之求解最长公共子序列问题 算法设计

求解最长公共子序列问题

一、【问题描述】字符序列的子序列是指从给定字符序列中随意地(不一定要联系)去掉若干个字符(可能一个也不去掉)后所形成的字符序列。给定两个序列A和B,称序列Z是A和B的公共子序列,是指Z同是A和B的子序列,该问题是求两序列A和B的最长公共子序列(LCS)

二、【问题求解】
若设A= (a0, a1, . am-1),B= (b0, b1, … bn-1) ,设Z= (z0, z1, .,zk-1) 为它们的最长公共子序列。不难证明有以下性质:

在这里插入图片描述
上图有一些难以理解,那么我们用更简单的方法来理解,假设有两个字符串A、B,分别有两个指针指向这两个字符串,设这两个指针分别为i和j,如今我们设定一个状态方程f(i,j)表示i指向字符串A的某个字符而j指向字符串B的某个字符的情况。那么此时就会有两种情况:
①两个指针所指向的字符相同:那么两个指针继续向右移动,此时的状态为f(i+1,j+1)+1;
②两个指针所指向的字符不相同:那么此时的状态又分为两种分别为f(i+1,j)和f(i,j+1)

因此我们定义一个二维动态规划数组dp,其中dp[i][j]为子序列A和B的最长公共子序列的长度。
那么状态转移方程如下:
dp[i][j]=0 (当i=0或者j=0即边界条件的情况下)
dp[i][j]=dp[i-1][j-1]+1 (当a[i-1]=b[i-1])
dp[i][j]=max(dp[i][j-1],dp[i-1][j]) (当a[i-1]!=b[j-1])

那么dp[m][n]就是我们要求的最终结果

总结:
用vector字符向量subs存放一个LCS,k=dp[m][n](LCS的字符个数),从k到1循环求出subs中的k个字符:
①如果dp[i][j]=dp[i-1][j](上方),说明a[i-1]或者b[j-1]不是LCS中的字符。
→→ i-1
②如果dp[i][j]=dp[i][j-1](左方),说明a[i-1]或者b[j-1]不是LCS中的字符。→→j-1
③其他情况,说明a[i-1]或者b[j-1]是LCS中的字符。
→→i-1,j-1,k-1表示求的字符减少一个

废话少说,放码过来:

//求解最长公共子序列问题的算法
#include <iostream>
#include <string.h>
#include <vector>
#include <string>
using namespace std;
#define max(x,y) ((x)>(y)?(x):(y))
#define MAX 51							//序列中最多的字符个数
//问题表示
int m,n;
string a,b;
//求解结果表示
int dp[MAX][MAX];						//动态规划数组
vector<char> subs;						//存放LCS
void LCSlength()						//求dp
{
	int i,j;
	for (i=0;i<=m;i++)					//将dp[i][0]置为0,边界条件
		dp[i][0]=0;
	for (j=0;j<=n;j++)					//将dp[0][j]置为0,边界条件
		dp[0][j]=0;
	for (i=1;i<=m;i++)
		for (j=1;j<=n;j++)				//两重for循环处理a、b的所有字符
		{	if (a[i-1]==b[j-1])			//情况(1)
				dp[i][j]=dp[i-1][j-1]+1;
			else						//情况(2)
				dp[i][j]=max(dp[i][j-1],dp[i-1][j]);

		}
}
void Buildsubs()					//由dp构造从subs
{
	int k=dp[m][n];					//k为a和b的最长公共子序列长度
	int i=m;
	int j=n;
	while (k>0)						//在subs中放入最长公共子序列(反向)
		if (dp[i][j]==dp[i-1][j])
			i--;
		else if (dp[i][j]==dp[i][j-1])
			j--;
		else
		{
			subs.push_back(a[i-1]);	//subs中添加a[i-1]
			i--; j--; k--;
		}
}
int main()
{
	a="abcbdb";
	b="acbbabdbb";
	m=a.length();			//m为a的长度
	n=b.length();			//n为b的长度
	LCSlength();			//求出dp
	Buildsubs();			//求出LCS
	cout << "求解结果" << endl;
	cout << "    a: " << a << endl;
	cout << "    b: " << b << endl;
	cout << "    最长公共子序列: ";
	vector<char>::reverse_iterator rit;
	for (rit=subs.rbegin();rit!=subs.rend();++rit)
		cout << *rit;
	cout << endl;
	cout << "    长度: " << dp[m][n] << endl;
	return 0;
}

运行结果:
在这里插入图片描述
debug代码:

//求解最长公共子序列问题的算法
#include <iostream>
#include <string.h>
#include <vector>
#include <string>
using namespace std;
#define max(x,y) ((x)>(y)?(x):(y))
#define MAX 51							//序列中最多的字符个数
//问题表示
int m,n;
string a,b;
//求解结果表示
int dp[MAX][MAX];						//动态规划数组
vector<char> subs;						//存放LCS
void LCSlength()						//求dp
{
	int i,j;
	for (i=0;i<=m;i++)					//将dp[i][0]置为0,边界条件
		dp[i][0]=0;
	for (j=0;j<=n;j++)					//将dp[0][j]置为0,边界条件
		dp[0][j]=0;
	for (i=1;i<=m;i++)
		for (j=1;j<=n;j++)				//两重for循环处理a、b的所有字符
		{	if (a[i-1]==b[j-1])			//情况(1)
				dp[i][j]=dp[i-1][j-1]+1;
			else						//情况(2)
				dp[i][j]=max(dp[i][j-1],dp[i-1][j]);

		}
}
void Buildsubs()					//由dp构造从subs
{
	printf("求LCS\n");
	int k=dp[m][n];					//k为a和b的最长公共子序列长度
	int i=m;
	int j=n;
	while (k>0)						//在subs中放入最长公共子序列(反向)
		if (dp[i][j]==dp[i-1][j])
			i--;
		else if (dp[i][j]==dp[i][j-1])
			j--;
		else
		{
			printf("i=%d,j=%d,添加元素a[i-1]=%c\n",i,j,a[i-1]);
			subs.push_back(a[i-1]);	//subs中添加a[i-1]
			i--; j--; k--;
		}
}
int main()
{
	a="abcbdb";
	b="acbbabdbb";
	m=a.length();			//m为a的长度
	n=b.length();			//n为b的长度
	LCSlength();			//求出dp
	printf("输出dp\n");
	for (int i=0;i<=n;i++)
	{
		for (int j=0;j<=n;j++)
			printf("%3d",dp[i][j]);
		printf("\n");
	}
	Buildsubs();			//求出LCS
	cout << "求解结果" << endl;
	cout << "    a: " << a << endl;
	cout << "    b: " << b << endl;
	cout << "    最长公共子序列: ";
	vector<char>::reverse_iterator rit;
	for (rit=subs.rbegin();rit!=subs.rend();++rit)
		cout << *rit;
	cout << endl;
	cout << "    长度: " << dp[m][n] << endl;
	return 0;
}

运行结果:
在这里插入图片描述
参考自教材《算法设计与分析》第二版 李春葆

  • 3
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值