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

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


先介绍几个定义:

  • 序列: 一个具有 严格次序 的对象元素的集合.
  • 字符序列 。 字符对象的序列则为 字符序列 如 X X X={ x 1 , x 2 , x m x_1, x_2, x_m x1,x2,xm}.
  • 子序列:给定序列 X X X={ x 1 , x 2 , x m x_1,x_2,x_m x1,x2,xm},序列 Z Z Z={ z 1 , z 2 , z k z_1, z_2, z_k z1,z2,zk}是 X X X的子序列,当且仅当存在一个严格递增的下标序列{ i 1 , i 2 , i k i_1,i_2,i_k i1,i2,ik}对 j ∈ j\in j{1,2,……,k},有 z j = x i j z_j= x_{i_j} zj=xij.
  • 前缀 :给定 X X X={ x 1 , x 2 , x m x_1,x_2, x_m x1,x2,xm}子序列 X i X_i Xi={ x 1 , x 2 , … … , x i x_1,x_2,……,x_i x1,x2,,xi}称为序列X的第i个前缀,其中 i ∈ i\in i{1,2,……,m}.
  • 公共子序列 :给定两个序列X和Y当另一个序列 Z Z Z既是 X X X的子序列,又是 Y Y Y的子序列时,称 Z Z Z X X X Y Y Y的公共子序列.
  • 最长公共子序列 (Longest Common Subsequence,LCS)问题:给定两个序列 X X X Y Y Y找出 X X X Y Y Y的一个最长公共子序列.
  • 注意:最长公共子序列和最长连续公共子序列是两个概念,这里是最长公共子序列,即不要求是连续的,可以隔着取,所以在理解下面的算法时,不要弄混了这两个概念,造成误解

例 : 给定两个序列 X X X={B,C,B,D,A,B}和 Y Y Y={ D,C,A,B,A} 则:

  • 序列{B,C,A}是 X X X Y Y Y的一个 公共子序列
  • 序列{C,B,A}是 X X X Y Y Y的一个公共子序列,它的长度是4,而且它是 X X X Y Y Y的一个最长公共子序列.
  • {C,A,B}也是最长公共子序列。可见,最长公共子序列并不唯一 。这里的 LCS 问题只需求出其中一个最长公共子序列即可.

算法思路:最优子结构与子问题重叠
设序列 X X X={ x 1 , x 2 , … , x m x_1, x_2,…,x_m x1,x2,,xm}.和 Y Y Y={ y 1 , y 2 , … , y n y_1,y_2,…,y_n y1,y2,,yn} 的最长公共子序列为 Z Z Z={ z 1 , z 2 , … , z k z_1,z_2,…,z_k z1,z2,,zk},则:

  • x m x_m xm= y n y_n yn,则 z k = x m = y n z_k=x_m=y_n zk=xm=yn,而且 z k − 1 是 z_{k-1}是 zk1x_{m-1} 和 和 y_{n-1}$的最长公共子序列;
  • x m ≠ y n x_m\ne{y_n} xm=yn z k ≠ x m z_k\ne{x_m} zk=xm,则 Z Z Z x m − 1 x_{m-1} xm1和Y的最长公共子序列.
  • x m ≠ y n x_m\ne{y_n} xm=yn z k ≠ y n z_k\ne{y_n} zk=yn,则 Z Z Z X X X y n − 1 y_{n-1} yn1的最长公共子序列.
  • 以上,可以使用反证法证明
  • 在计算 X X X Y Y Y的LCS时,可能需要计算 X m − 1 X_{m-1} Xm1 Y Y Y X X X Y n − 1 Y_{n-1} Yn1的LCS很显然,都包含了 X m − 1 X_{m-1} Xm1 Y n − 1 Y_{n-1} Yn1的LCS.
  • 建立最优值的递归关系:
    • 用arr[i][j].n记录序列 X i X_i Xi Y j Y_j Yj的LCS的长度。其中, X i X_i Xi={ x 1 , x 2 , … , x i x_1,x_2,…,x_i x1,x2,,xi}和 Y j Y_j Yj={ y 1 , y 2 , … , y j y_1,y_2,…,y_j y1,y2,,yj}分别为序列 X X X Y Y Y的第i个前缀和第j个前缀。
    • 当i=0或j=0时,空序列是 X i X_i Xi Y j Y_j Yj的LCS。此时C[i][j]=0, 其他情况下,由最优子结构性质可建立递归关系。
    • a r r [ i ] [ j ] . n = { 0 i=0,j=0 a r r [ i − 1 ] [ j − 1 ] . n + 1 i,j>0; x i = y j m a x { a r r [ i ] [ j − 1 ] . n , a r r [ i − 1 ] [ j ] . n } i,j>0; x i ≠ y j arr[i][j].n= \begin{cases} 0& \text{i=0,j=0}\\ arr[i-1][j-1].n + 1& \text{i,j>0;$x_i=y_j$}\\ max\{arr[i][j-1].n,arr[i-1][j].n\} & \text{i,j>0;$x_i\ne{y_j}$} \end{cases} arr[i][j].n=0arr[i1][j1].n+1max{arr[i][j1].n,arr[i1][j].n}i=0,j=0i,j>0;xi=yji,j>0;xi=yj

求解步骤:

  • Step1 确定合适的数据结构.采用二维数组arr.n来存放各个子问题的最优解。二维数组arr.mask记录各子问题最优值的来源。
  • Step2 初始化。令arr[i][0].n=0,arr[0][j].n=0,其中 0 ≤ i ≤ m , 0 ≤ j ≤ n 0\leq i\leq m,0\leq j\leq n 0im,0jn
  • Step3 循环阶段。根据递归关系式确定 X i X_i Xi Y j Y_j Yj的LCS的长度 0 ≤ i ≤ m 0\leq i\leq m 0im。对于每个i循环 0 ≤ j ≤ n 0\leq j\leq n 0jn.
  • Step4 根据arr.mask记录的信息以自底向上的方式来构造该LCS问题的最优解.

在这里插入图片描述

代码如下:

#include <iostream>
#include <cstring>

using namespace std;

#define RIGHT 0
#define DOWN 1
#define LOW_RIGHT 2

struct Node {
	/**
	 * 当前位置i行j列
	 * a的前i个元素和b的前j个元素的最长公共子序列
	 */
	int n;
	/**
	 * 标记是从哪走过来的,方便进行回溯
	 * RIGHT 从左边向右走过来的
	 * DOWN 从上向下走过来的
	 * LOW_RIGHT 从左上向右下走过来的
	 */
	int mask;
	/*默认构造函数*/
	Node() {
		n = 0;
		mask = -1;
	}
};

//显示当前的标记数组n的信息
void disp_n(Node** arr, int i, int j) {
	for (int tmp_i = 0; tmp_i < i; tmp_i++) {
		for (int tmp_j = 0; tmp_j < j; tmp_j++) {
			cout << arr[tmp_i][tmp_j].n << "\t";
		}
		cout << endl;
	}
	cout << "----------------------------------------------------" << endl;
}

//显示当前的标记数组mask的信息
void disp_dir(Node** arr, int i, int j) {
	for (int tmp_i = 0; tmp_i < i; tmp_i++) {
		for (int tmp_j = 0; tmp_j < j; tmp_j++) {
			cout << arr[tmp_i][tmp_j].mask << "\t";
		}
		cout << endl;
	}
	cout << "---------------------------------------------------" << endl;
}


string LCS(const string &a, const string &b) {
	/**
	 * 直接将a和b的长度进行计算,这样就不用每次调用的时候,还调用一遍,
	 * 这样能降低时间复杂度
	 */
	int len_a = a.length(), len_b = b.length();
	// 记录和回溯数组
	Node** arr = new Node * [len_a + 1];
	for (int i = 0; i <= len_a; i++) {
		arr[i] = new Node[len_b + 1];
	}
	/**
	 * 将第0行和第0列的n值都直接赋值为0,
	 * 因为,前(0,j)和(i,0)必定为0
	 * (0,j): a的前0个元素和b的前j个元素的最长公共子序列
	 * (i,0): b的前0个元素和a的前i个元素的最长公共子序列
	 */
	for (int i = 0; i <= len_a; i++) {
		for (int j = 0; j <= len_b; j++) {
			arr[i][j].n = 0;
		}
	}

	for (int i = 1; i <= len_a; i++) {
		for (int j = 1; j <= len_b; j++) {
			/**
			 * i, j 都是从1开始的,所以为(i-1)和(j-1)
			 */
			if (a[i - 1] == b[j - 1]) {
				/**
				 * arr[i - 1][j - 1].n : a的前(i-1)项和b的前(j-1)的项的最长公共子序列
				 * arr[i][j].n : a的前i项和b的前j项的最长公共子序列的和
				 */
				arr[i][j].n = arr[i - 1][j - 1].n + 1;
				// 标记是从哪里走过来的
				arr[i][j].mask = LOW_RIGHT;
			}
			else {
				/**
				 * a[i] 和 b[j] 不相等时
				 * arr[i][j] = max{arr[i][j-1].n, arr[i-1][j].n}
				 */
				if (arr[i][j - 1].n > arr[i - 1][j].n ) {
					arr[i][j].n = arr[i][j - 1].n;
					// 标记从哪里走过来的
					arr[i][j].mask = RIGHT;
				}
				else {
					arr[i][j].n = arr[i - 1][j].n;
					// 标记从哪里走过来的
					arr[i][j].mask = DOWN;
				}
			}
		}
		
	}

	// 显示arr的n信息
	disp_n(arr, len_a + 1, len_b + 1);
	// 显示arr的mask信息
	disp_dir(arr, len_a + 1, len_b + 1);

	// 最终返回的公共子序列的字符串
	string lcs = "";
	// 从最后一个位置开始回溯,在mask为-1时停止
	for (int i = len_a, j = len_b; arr[i][j].mask != -1; ) {
		// 从上向下走过来的
		if (arr[i][j].mask == DOWN ) {
			// 向上走回去
			i--;
			continue;
		}
		// 从左向右走过来的
		if (arr[i][j].mask == RIGHT ) {
			// 向左走回去
			j--;
			continue;
		}
		// 从左上向右下走过来的
		if (arr[i][j].mask == LOW_RIGHT) {
			// 这种情况发生就意味着a[i-1] = b[j-1]的成立,所以,属于最长公共子序列的元素,记录下来
			lcs.push_back(a[i - 1]);
			// 向左上走回去
			i--;
			j--;
			continue;
		}
	}

	// 释放内存空间
	for (int i = 0; i <= len_a; i++) {
		delete arr[i];
	}
	delete[]arr;

	// 因为是回溯回去的,所以这里要将记录字符串翻转一下
	reverse(lcs.begin(), lcs.end() );
	return lcs;
}

int main() {
	string a, b;

	// 输入两字符串
	cin >> a >> b;

	// 调用函数,得到最长公共子串
	string lcs = LCS(a, b);

	cout << "最长公共子序列为 : " << lcs << endl;
	cout << "长度为 : " << lcs.length() << endl;
	return 0;
}

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

中小庸

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值