动态规划——最长公共子序列

动态规划——最长公共子序列

设计与实现最长公共子序列问题的算法;

解决思路

首先需要明确子序列的概念,子序列与子串并不是一种东西,子序列我们可以理解为在一个序列中删除任意多个字符,剩余字符按原有顺序组成的序列就是原序列的子序列,其中删除的字符可以使连续的也可以是不连续的,对于任意一个字符来说,有删除与不删除两种选择,那么对于一个长为n的序列,他就有2^n个子序列。
我们想求两个序列的最长公共子序列,最简单的方法就是生成一个序列的所有子序列,然后检查这些子序列是否是另一个序列的子序列,并找到最长的公共子序列。这个方法其中的难点在于如何生成一个序列的所有子序列,方法一时利用二进制掩码来决定序列的某一位是否应该删除,然而这种方法涉及到了移位运算,int型变量有32位,因此当序列长度超过32时就会出错,即使换成long也只有64位,因此这种方法局限性很大。
另一种可以产生所有子序列的方法就是深度优先搜索,我们可以将母序列想象成一个迷宫,每一个字符都是迷宫的路径,而选择留下该字符和不留下该字符就是路径的两条分叉路,而已经遍历到了母序列的终点就是迷宫的死路,当已经遍历到死路时就回溯到上一层就,直至回到起点就可以得到所有子序列。这样的方法虽然可以克服掩码的确定,然而总共有2^n个子序列,因此无论子序列与另一个序列的比较过程有多简洁,它的时间复杂度也是指数级的,因此该方法我们了解了就好,不应当在我们的考虑范围内。
那么如何使用动态规划来解决这个问题呢?我们记两个序列分别为X={x1,x2,x3……xm}、Y={y1,y2,y3……yn},他们的一个公共子序列为Z={z1,z2,z3……zk},Z中的所有元素必定是XY共有的, 但是位置是不确定的,我们可以由此得到如下结论:
①若xm = yn,则zk = xm = yn,那么Zk-1为Xm-1和Yn-1的公共子序列
②若xm != yn,且zk != xm,那么Z是Xm-1和Y的公共子序列
③若xm != yn,且zk != yn,那么Z是X和Yn-1的公共子序列
根据以上结论,我们可以用c[i][j]来表示X的前i个字符与Y的前j个字符的最长公共子序列的长度,我们可以得到以下递推公式:
在这里插入图片描述
但是这样我们只能得到最长公共子序列的长度,如果我们希望得到最长公共子序列,我们还需要增加一个追踪函数b[i][j],递推公式和C[i][j]类似,如下:
在这里插入图片描述
当我们得到最长公共子序列的长度后,就可以根据箭头往回追踪得到最长公共子序列

算法描述

算法一、LCS_dp
输入:序列str1={x1,x2,x3……xm},序列str2={y1,y2,y3……yn}
①for i=0 to m c[i][0]=0
②for j=0 to n c[0][j]=0
③for i = 1 to m
④ for j = 1 to n
⑤ if str1[i-1]==str[j-1] then c[i][j] = c[i-1][j-1]+1,b[i][j]=↖
⑥ else if c[i-1][j] >= c[i][j-1] then c[i][j] = c[i-1][j],b[i][j]=↑
⑦ else c[i][j] = c[i][j-1],b[i][j]=←
⑧return c[m][n]
算法二、return_res(用于返回最长子序列)
输入:序列str1={x1,x2,x3……xm},序列str2={y1,y2,y3……yn},追踪矩阵b[n][m]
①i = m, j = n, res=””
②while i>0 && j>0 do
③ if str1[i-1] == str2[j-1] then res.insert(0,str[i-1])
④ if b[i][j] == ↖ then i–,j–
⑤ else if b[i][j] == ↑ then i–
⑥ else if b[i][j] = ← then j—
⑦return res

算法代码

求最长公共子序列长度,填充追踪矩阵的代码:

int LCS_dp(int n1, int n2)
{
	for (int i = 0; i <= n1; i++)c[i][0] = 0;
	for (int j = 0; j <= n2; j++)c[0][j] = 0;
	for (int i = 1; i <= n1; i++)
	{
		for (int j = 1; j <= n2; j++)
		{
			if (str1[i-1] == str2[j-1]) 
			{
				c[i][j] = c[i - 1][j - 1] + 1;
				b[i][j] = 'c';
			}
			else if (c[i - 1][j] >= c[i][j - 1]) 
			{
				c[i][j] = c[i - 1][j];
				b[i][j] = 'u';
			}
			else
			{
				c[i][j] = c[i][j - 1];
				b[i][j] = 'l';
			}
		}
	}
	return c[n1][n2];
}

得到最长公共子序列的代码:

string return_res(int n1, int n2)
{
	int i = n1, j = n2;
	string res = "";
	while (i > 0 && j > 0)
	{
		if (str1[i - 1] == str2[j - 1])
		{
			string temp = "";
			temp += str1[i - 1];
			res.insert(0, temp);
		}
		if (b[i][j] == 'c')i--, j--;
		else if (b[i][j] == 'u')i--;
		else if (b[i][j] == 'l')j--;
	}
	return res;
}

算法分析

动态规划算法嵌套了两层循环,外层循环次数取决于序列1的长度n1,而内层循环的次数取决于序列2的长度n2,因此算法的时间复杂度为O(n1*n2)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值