最长公共子序列

1.定义

给定一个序列,在删去若干个元素后得到的序列即为子序列。

图1 子序列

如图1所示,序列{A, B, C, B, D, A, B}的其中一个子序列就是{B, C, B, A}。

图2 公共子序列

 如图2,序列{A, B, C, B, D, A, B}和序列{B, D, C, A, B, A}的其中一个子序列就是{B, C, B, A},并且这个公共子序列是最长公共子序列(不唯一)。

2.动态规划

能使用动态规划解决的问题需要具有 最优子结构重叠子问题的性质。

假设X = {x_{1}, x_{2}, ..., x_{}m} 和Y={y_{}1, y_{2}, ..., y_{n}} 的最长公共子序列为Z = {z_{1}, z_{2}, ..., z_{k}},那么:

  1. 如果x_{}m = y_{n},显然有z_{k}x_{}m = y_{n},即Z_{k-1}X_{m-1}Y_{n-1}的最长公共子序列;
  2. 如果x_{}m != y_{n}, 且z_{k} != x_{}m, 则Z_{k}X_{m-1}Y_{n}的最长公共子序列;
  3. 如果x_{}m != y_{n}, 且z_{k} != y_{n}, 则Z_{k}X_{m}Y_{n-1}的最长公共子序列。

 从上面的分析可知,对于最长公共子序列,显然具有最优子结构问题,即原问题的最优解包含着子问题的最优解。

要找出X和Y的最长公共子序列,可以递归进行:

  1. x_{}m = y_{n}时,找出X_{m-1}Y_{n-1}的最长公共子序列,然后再加上zk即可得到X和Y的最长公共子序列;
  2. x_{}m != y_{n}时,则需要找出X_{m-1}Y_{n}的最长公共子序列以及X_{m}Y_{n-1}的最长公共子序列,找出两个的最长公共子序列即为所求。

从递归结构来看,显然也具有重叠子问题的性质。由此,我们可以得到以下的式子:

图3 推导公式

 3.求解

3.1 递归求解

可以直接根据上面的推导公式直接得到对应的代码:

int LCSLength(const char* x, const char* y, int i, int j){
    if (i == 0 || j == 0)
        return 0;
    else if (x[i] == y[j])
        return LCSLength(x, y, i - 1, j - 1) + 1;
    else
        return max(LCSLength(x, y, i, j - 1), LCSLength(x, y, i - 1, j));
}

上述代码完全是按照图3的推导公式得到的,递归看上去很是简单,但是里面包含了大量的重复计算。

3.2 动态规划

 动态规划是自底向上进行计算的,相较于递归,它可以显著提高算法的效率。

/**
 * 动态规划自底向上求解
 * @param m: 字符串x的长度
 * @param n: 字符串y的长度
 * @param x: 字符串x
 * @param y: 字符串y
 * @param c: c为二维数组c[i][j]记录的是Xi和Yj的最长公共子序列的长度
 * @param b: b为二维数组,在这里表现为路径,可以较为方便地获取到最长公共子序列
*/
void LCSLength(int m, int n,const char* x,const char* y, int*** c, int*** b) {
	//第0行和第0列默认为0 因此共 m + 1行 n + 1列
	int** answer = new int* [m + 1];
	int** path = new int* [m + 1];
	for (int i = 0; i <= m; i++) {
		answer[i] = new int[n + 1];
		memset(answer[i], 0, sizeof(int) * (n + 1));

		path[i] = new int[n + 1];
		memset(path[i], 0, sizeof(int) * (n + 1));
	}
    //重点在于二重循环
	int i = 0, j = 0;
	for (i = 1; i <= m; i++) {
		for (j = 1; j <= n; j++) {
			if (x[i - 1] == y[j - 1]) {
				answer[i][j] = answer[i - 1][j - 1] + 1;
				path[i][j] = 1;
			}
			else if (answer[i - 1][j] >= answer[i][j - 1]) {
				answer[i][j] = answer[i - 1][j];
				path[i][j] = 2;
			}
			else {
				answer[i][j] = answer[i][j - 1];
				path[i][j] = 3;
			}
		}
	}
	if (c != nullptr)
		* c = answer;
	if (b != nullptr)
		* b = path;
}

LCSLength函数存在着两个输出,分别是c和b这两个二维数组,当传入时,只需要传入一个二维数组的地址即可,对应的内存会在LCSLength函数内部进行申请。

可以看到,c对应的变量名称为answerb对应的则是path;它们明显都比字符串的长度多了一个单位,这样做的目的是为了避免边界条件的判断。

图4 answer图初始

在初始状态下 c[0][] 和 c[][0] 的元素值都为0(其他元素没必要设为0,因为之后的双重循环中会对每一个元素都进行赋值,这里只是用了memset函数全部设为了默认值0) 

接下来,进行简单地推导一下:

令x = "ABCBDAB" y = "BDCABA";

一开始i = 1, j = 1。然后x[0] != y[0],此时有从c[1][1] = c[i-1][j]=0;

i = 1, j = 2, x[0] != y[1],此时有c[1][2] = 0;

i = 1, j = 3, x[0] != y[2],此时c[1][3] = 0;

i = 1, j =4, x[0] == y[3],此时c[1][4] = c[0][3] + 1 = 1;

以此类推。。。
图5 完整数据

 如上图所示,之所以加上了额外的一行以及一列的值为0的元素,是为了简化循环中的判断条件,它起到的作用类似于单链表中的表头(避免了边界的判断)。

3.2.1 最长公共子序列的获取

在代码中,还存在着一个名为b的二维数组,它的作用是较为便利地获取最长公共子序列:

/**
 * 输出最长公共子序列的其中一个
 * @param i: 字符串x的长度
 * @param j: 字符串y的长度
 * @param x: 字符串
 * @param path: 路径
*/
void LCS(int i, int j,const char* x, int** path) {
	stack<char> stack;

	while (i != 0 && j != 0) {
		if (path[i][j] == 1) {
			stack.push(x[i - 1]);
			i -= 1;
			j -= 1;
		}
		else if (path[i][j] == 2)
			i -= 1;
		else if (path[i][j] == 3)
			j -= 1;
	}
	while ( !stack.empty())
	{
		cout << stack.top();
		stack.pop();
	}
}

LCS函数根据b获取到X和Y的最长公共子序列之一,其内部使用了栈来代替递归。在LCSLength中,只有x[i - 1] == y[j - 1]时,才会设置path[i][j] = 1,否则获取max(c[ i ][j - 1], c[i - 1][ j ]),并赋予path[i][j]2或者是3。在LCS函数中则根据LCSLength中设置的值来进行逆推。

其实b这个数组是可以被c代替的。数组c[i][j]的值仅由c[i - 1][j - 1]、c[ i ][j - 1]以及c[i - 1][ j ]这三个元素的值所确定。

void LCS(const char* x, const char* y, int i, int j, int** c) {
	stack<char> stack;

	while (i > 0 && j > 0) {
		if (c[i][j] == c[i][j - 1] + 1) {
			stack.push(x[i - 1]);
			j = j - 1;
			i = i - 1;
		}
		else if (c[i][j] == c[i - 1][j] + 1) {
			stack.push(x[i - 1]);
			i = i - 1;
			j = j - 1;
		}
		else {
			//j = j - 1;
			i = i - 1;
		}
	}
	while (!stack.empty())
	{
		cout << stack.top();
		stack.pop();
	}
	cout << endl;
}

无论有没有用到b数组,上面的两个LCS函数目前都是只能获取最长公共子序列之一。这种情况在第二个LCS函数中可以简单地修改else的语句块中是让i --还是j--或者是同时进行,以此来决定当前c[ i ][ j ]选择哪一个方向。

3.2.2 最长公共子序列的长度

由于在LCSLength函数中,c[ i ][ j ]表示的就是Xi Yj的最长公共子序列的长度,这里可以简单地查数组就可以知道。不过,如果只是获取最长公共子序列的长度的话,可以只需要两行的数组空间来交替使用。从LCSLength的代码中可以了解到,数组c[i][j]的值仅由c[i - 1][j - 1]、c[ i ][j - 1]以及c[i - 1][ j ]这三个元素的值所确定。c[i - 1][j - 1] c[i - 1][j]是前一行,c[ i ][j - 1]则是当前行中之前已经计算得到。这两行数组交替使用则可以确定最长公共子序列的长度。

4.参考

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值