动态规划求两个字符串的最长公共子串和最长公共序列

@YangYang48

求两个字符串的最长公共子串和最长公共序列

基本概念

最长公共子串(Longest Common Substring)问题是寻找两个或多个已知字符串最长的子串,且子串却必须是连续的
最长公共子序列(Longest Common Subsequence)问题的区别在于子序列且子串不是连续的

问题描述

有两个字符串str和str2,求出两个字符串中最长公共子串长度。

示例 1:

str1 = “acbcbcef”
str2 = “abcbced”
str和str2的最长公共子串为"bcbce"
最长公共子串长度为5

示例 2:

str1 = “acbcbcef”
str2 = “abcbced”
str和str2的最长公共子串为"BDAB”、“BCAB”、“BCBA”。
最长公共子序列的长度4。

最长公共子串

这里以c/c++语言来具体化

  1. 建立一个二维矩阵,其行列分别对应两个字符串
  2. 比较二维矩阵中行列相等的点,相等即设置标记点为1
  3. 之后重复操作,再次遇到行列相等点则,在累计标记点值(累计标记点即为最长公共子串)

动态规划求两个字符串的最长公共子串和最长公共序列_第1张图片

代码

/*最长公共子串动态规划表*/
string getLCS(string str1, string str2) 
{
	vector<vector<int> > record(str1.length(), vector<int>(str2.length()));
	int maxLen = 0, maxEnd = 0;/*maxlen累计标记点,maxEnd为最长子串的最后一个元素*/
	for(int i=0; i<static_cast<int>(str1.length()); ++i)
	{
		for (int j = 0; j < static_cast<int>(str2.length()); ++j)
		{
			if (str1[i] == str2[j]) 
			{
				if (i == 0 || j == 0) 
				{
					record[i][j] = 1;
				}
				else 
				{
					record[i][j] = record[i - 1][j - 1] + 1;
				}
			}
			else 
			{
				record[i][j] = 0;/*i和j为其他数都为0*/
			}
		}	
		if (record[i][j] > maxLen) 
		{
			maxLen = record[i][j];
			maxEnd = i; //若记录i,则最后获取LCS时是取str1的子串
		}
	}
	return str1.substr(maxEnd - maxLen + 1, maxLen);
}

这个动态数组在计算的达效的同时,还可以保存长度和末位的元素,通过string类中的substr操作来提取出最长公共子串

最长公共序列

理解了上述的二维矩阵概念,这个最长公共序列就好理解了

  1. 建立一个二维矩阵,其行列分别对应两个字符串
  2. 比较二维矩阵中行列相等的点,相等即设置标记点为1,遇到不相等的点取左边和上面最大值
  3. 之后重复操作,再次遇到行列相等点则,在累计标记点值
  4. 二维矩阵的末位点即为所求最长公共序列的长度

注意:这里的动态规划表为table[str1.length()+1][str2.length()+1]

解决了最长公共序列的长度,那么怎么进行输出子序列呢?
我们需要在动态规划表上进行回溯,具体见下面操作

  1. 如果格子table[i][j]对应的str1[i-1] == str2[j-1],则把这个字符放入 LCS 中,并在该点斜对角线的上一个点中继续进行判断;

  2. 如果格子table[i][j]对应的 str1[i-1] ≠ str2[j-1],则比较左边和上面的值,跳入值较大的格子继续进行判断;

  3. 如果格子table[i][j]对应的 str1[i-1] ≠ str2[j-1],左上两值相等,跳入值左边和上面的格子继续进行判断;

  4. 可以进行递归操作直到 i 或 j 小于等于零为止,倒序输出 LCS 。
    动态规划求两个字符串的最长公共子串和最长公共序列_第2张图片

代码

/*动态规划最长公共序列,并输出子序列*/
#include 
#include 
#include 
#include 
using namespace std;
 
string str1 = "ABCBDAB";
string str2 = "BDCABA";
vector<vector<int>> table; // 动态规划表
set<string> setOfLCS;      // set保存所有的LCS
int max(int a, int b)
{
	return (a>b)? a:b;
}
/* 对字符串进行逆序操作 */
string Reverse(string str)
{
	int low = 0;
	int high = str.length() - 1;
	while (low < high)
	{
		char temp = str[low];
		str[low] = str[high];
		str[high] = temp;
		++low;
		--high;
	}
	return str;
}
/* 同最长公共子串相同,返回最长序列长度*/
int lcs(int m, int n)
{
	// 表的大小为(m+1)*(n+1)
	table = vector<vector<int>>(m+1,vector<int>(n+1));
	for(int i=0; i<m+1; ++i)
	{
		for(int j=0; j<n+1; ++j)
		{
			// 第一行和第一列置0
			if (i == 0 || j == 0)
				table[i][j] = 0;
			else if(str1[i-1] == str2[j-1])
				table[i][j] = table[i-1][j-1] + 1;
			else
				table[i][j] = max(table[i-1][j], table[i][j-1]);
		}
	}
	return table[m][n];
}
/*求出所有的最长公共子序列,并放入set中 */
void traceBack(int i, int j, string lcs_str)
{
	while (i>0 && j>0)
	{
		if (str1[i-1] == str2[j-1])
		{
			lcs_str.push_back(str1[i-1]);
			--i;
			--j;
		}
		else
		{
			if (table[i-1][j] > table[i][j-1])
				--i;
			else if (table[i-1][j] < table[i][j-1])
				--j;
			else   // 相等的情况
			{
				traceBack(i-1, j, lcs_str);
				traceBack(i, j-1, lcs_str);
				return;
			}
		}
	}
	setOfLCS.insert(Reverse(lcs_str));
}
int main()
{
	int m = str1.length();
	int n = str2.length();
	int length = lcs(m, n);
	cout << "The length of LCS is " << length << endl;
	string str;
	traceBack(m, n, str);
	set<string>::iterator beg = setOfLCS.begin();
	for( ; beg!=setOfLCS.end(); ++beg)
		cout << *beg << endl;
	getchar();
	return 0;
}

动态规划表相对于传统的递归或者是暴力查找更优,时间复杂度o(mn),空间复杂度o(mn)
如果本文对宝宝打开思路有帮助,可以点个赞哦~

参考

[1]南方以北_求两个字符串的最长公共子串_2018_8_12
[2]神奕_【动态规划】Dynamic Programming_2014_11_27
[3]神奕_【动态规划】输出所有的最长公共子序列_2014_11_29

你可能感兴趣的:(2019-8,c/c++)