推荐参考该博文:java实现字符串匹配问题之求两个字符串的最大公共子串
当然这篇也一样,看个人理解:求两个字符串的最长公共子串
图论中的最短路径算法。
大致分有:迪杰斯特拉算法(Dijkstra)和弗洛伊德算法(Floyd)。
(对应着 贪心算法和动态规划 …… 别慌,名字起的高大尚并无影响理解。。。)
数据结构算法编程课、离散数学课、计算机网络课等都会涉及该算法。
本质都是化作矩阵,故线性代数一定要好好学。
最好的理解方式是什么:
亲自动手 —— 图解,自己手动在草稿纸上推演一遍(小矩阵即可)。
太长时间没写C了,一入python差些找不着回头路(哈哈)
#include
#include
#include
#include
using namespace std;
typedef vector<string> VS;
// 为避免重复,检查当前子串是否为不存在,不存在时返回true
bool IsNoRepetition(string& , vector<string>& );
int main()
{
string s1, s2; // 待输入的两字符串
string sameSubString; // 临时存储相同子串
VS sameSubStringVector; // 保存所有相同子串
while (cin >> s1 >> s2) {
int max = 0; // 最长子串长度(字符元素个数),初始置0、默认无相同子串
int row = s1.length(); // 矩阵行数(长度)
int col = s2.size(); // 矩阵列数(宽度)
sameSubString.clear(); // 初始化时,需要重置中间存储变量(清空)
// 申请动态二维数组
// 也可以可直接 vector>不过建议多多学习、开拓视野
int** dp = new int *[row];
if (dp) {
for (int i = 0; i < row; ++i) {
dp[i] = new int[col];
}
}
// 初始化矩阵,全部置false(即首先默认字符串不相同,其后相同再+1)
// 此处是为了提醒学弟,注意学习 memset和fill的区别
for (int i = 0; i < row; ++i) {
/*for (int j = 0; j < col; ++j) {
dp[i][j] = 0;
}*/
fill(dp[i], dp[i] + col, 0); // 两种初始化方式
}
for (int i = 0; i < row; ++i) {
for (int j = 0; j < col; ++j) {
int iTemp = i, jTemp = j; // 临时变量
while (s1[iTemp] == s2[jTemp]) {
dp[iTemp][jTemp] = dp[iTemp][jTemp] + 1;
sameSubString += s1[iTemp];
iTemp++;
jTemp++;
// 横纵都 +1是为了斜对角线(即 s1和s2串都往后移动一位)
// 值得注意的是别造成数组越界(程序健壮性问题、bug)
if (iTemp == row || jTemp == col) {
break;
}
}
// 相同子串不为空(即存在时)
if (!sameSubString.empty()) {
//cout << "sameSubString = " << sameSubString << endl; // 通过输出测试结果,是否如预期所想
if (IsNoRepetition(sameSubString, sameSubStringVector)) {
sameSubStringVector.push_back(sameSubString);
}
sameSubString.clear(); // 每遍历过一次相同子串,最后记得重置为空(细节)
}
}
}
// 矩阵变换完成后,查找最大值(即为最长相同子串长度)
for (int i = 0; i < row; i++) {
for (int j = 0; j < col; j++) {
if (max < dp[i][j]) {
max = dp[i][j];
}
}
}
if (sameSubStringVector.empty()) {
cout << endl;
}
else {
// 将各个相同子串按照字典顺序排序
sort(sameSubStringVector.begin(), sameSubStringVector.end());
for (VS::iterator iter = sameSubStringVector.begin(); iter != sameSubStringVector.end(); ++iter) {
// 直接输出的所有的相同子串
//cout << *iter << endl;
// 使用条件判断只输出最长的相同子串
if ((*iter).size() == max) {
cout << *iter << endl;
break; // break是为了只输出一个最长的公共子串,即ASCLL码最小的那个
}
}
}
//cout << endl; // 此空行是为了排版好看,避免pe格式出错
// 每执行一遍程序,重置为初始状态(为空)。至于,不放在else内,是编码经验释然。
sameSubStringVector.clear();
// new了内存空间就要delete
// 注意这种表达方式
for (int i = 0; i < row; ++i) {
delete[] dp[i];
}
delete[] dp;
}
return 0;
}
bool IsNoRepetition(string& str, vector<string>& vs) {
for (int i = 0; i < vs.size(); ++i) {
if (vs[i] == str)
return false; //有重
}
return true; //无重
}
对于每组测试数据,输出最大子串。
如果最大子串为空(即不存在),则输出一个空行。
输入:
abcded123456aabbcc
abcdaa1234
输出:
1234
本人代码很平民化了,如果看了不能理解实在是……不敢恭维你的编程基础。
实在不理解的话,可以评论区留言或者私信本人账号。
当然,[email protected]发送邮件或者添加好友也可。只要笔者上线。
为何此处说即可理解最长公共子串、最长公共子序列?
因为只需要理解了理论知识部分(其实就是极其简单的逐个字符匹配问题),
代码只需要修改一个条件即可从最长相同子串转为最长相同子序列:
即对while (s1[iTemp] == s2[jTemp])循环进行相应的修改。
最长公共子串:字符一直匹配直到字符不再相同或者已经遍历完较短字符串;
最长公共子序列:一直遍历至较短字符串结束即可,当前字符不相同也要继续匹配下一对字符(各自向后挪动一位)
由一道公共子串题目引起的自我反思
============ 我是分割线 ============
推荐博客:LCS(最长公共子序列)
讲解的很好了,以至于自己发现自己上边对最长公共子序列的理解过于想当然了。
上边的理解偏差在于:如何保证是在已有子序列的基础上去继续匹配下一对,这才是子序列的关键和难点。
#include
#include
#include
#include
using namespace std;
void printDP(int** dp, const int& row, const int& col) {
for (int i = 0; i < row; ++i) {
for (int j = 0; j < col; ++j) {
if (j != col - 1) {
cout << dp[i][j] << "\t";
}
else {
cout << dp[i][j] << endl;
}
}
}
}
int main()
{
string s1, s2; // 待输入的两字符串
string longestCommonSubsequence; // 最长相同子序列;
while (cin >> s1 >> s2) {
int row = s1.length() + 1; // 矩阵行数(长度);
int col = s2.size() + 1; // 矩阵列数(宽度);
longestCommonSubsequence.clear(); // 初始化时,需要重置为空;
// 申请动态二维数组。也可以可直接 vector>不过建议多多学习、开拓视野;
// 先申请一列,该列的每个元素对应一个一维数组(一行);再每个元素位申请一行。(行、列都仅仅是指一维数组);
int** dp = new int *[row];
if (dp) {
for (int i = 0; i < row; ++i) {
dp[i] = new int[col];
}
}
// 初始化矩阵,全部置false(即首先默认字符串不相同,其后相同再+1)。注意学习 memset和fill的区别;
for (int i = 0; i < row; ++i) {
//dp[i][0] = 0; // 矩阵第一列全都置0
fill(dp[i], dp[i] + col, 0);
}
//for (int j = 0; j < col; ++j) {
// dp[0][j] = 0; // 矩阵第一行全部置0
//}
//printDP(dp, row, col);
// 注意内存空间范围,数组别越界了;
for (int i = 0; i < row - 1; ++i) {
for (int j = 0; j < col - 1; ++j) {
相等时,在已有的共同子序列的基础上,共同序列长度 +1;
对进行字符的比对时,记得 i、j 要 -1(即从开头起);
//if (s1[i] == s2[j]){
// dp[i + 1][j + 1] = dp[i][j] + 1;
//}
如何理解?——在已有序列的基础上,字串末尾添加不等的字符而已
//else {
// dp[i + 1][j + 1] = max(dp[i][j + 1], dp[i + 1][j]);
//}
// 若是只输出长度而不要求保存共同子序列的字符,则可以三目运算符(加括号是为了可读性、便于读者理解代码)
dp[i + 1][j + 1] = (s1[i] == s2[j] ? dp[i][j] + 1 : max(dp[i][j + 1], dp[i + 1][j]));
}
}
回溯,通过路径拼凑出LCS
int i = row - 1;
int j = col - 1;
while (i > 0 && j > 0) {
cout << "i = " << i << "\t" << "j = " << j << "\t\t";
cout << "dp[i][j] = " << dp[i][j] << "\t" << "dp[i-1][j-1]" << dp[i - 1][j - 1] << "\t\t";
cout << "s1[i-1] = " << s1[i - 1] << "\t" << "s2[j-1] = " << s2[j - 1] << endl;
if (dp[i][j] == dp[i - 1][j - 1] + 1 && s1[i - 1] == s2[j - 1]) {
if (i - 1 >= 0 && j - 1 >= 0) {
longestCommonSubsequence = s1[i - 1] + longestCommonSubsequence;
cout << "1" << "\t" << "longestCommonSubsequence = " << longestCommonSubsequence << endl;
}
--i;
--j;
// 走斜线(往左上方);
}
else if (dp[i - 1][j] > dp[i][j - 1]) {
if (i - 1 >= 0 && j - 1 >= 0 && s1[i - 1] == s2[j - 1]) {
longestCommonSubsequence = s1[i - 1] + longestCommonSubsequence;
cout << "2" << "\t" << "longestCommonSubsequence = " << longestCommonSubsequence << endl;
}
--i;
// 竖着走(往上);
}
else if (dp[i - 1][j] < dp[i][j - 1]) {
if (i - 1 >= 0 && j - 1 >= 0 && s1[i - 1] == s2[j - 1]) {
longestCommonSubsequence = s1[i - 1] + longestCommonSubsequence;
cout << "3" << "\t" << "longestCommonSubsequence = " << longestCommonSubsequence << endl;
}
--j;
// 横着走(往左);
}
else {
if (i - 1 >= 0 && j - 1 >= 0 && s1[i - 1] == s2[j - 1]) {
longestCommonSubsequence = s1[i - 1] + longestCommonSubsequence;
cout << "4" << "\t" << "longestCommonSubsequence = " << longestCommonSubsequence << endl;
}
//--i;
--j;
// 横竖都行,往上、往左二选一,选择不同、最长公共子串的结果不同;
}
/*if (i - 1 >= 0 && j - 1 >= 0 && s1[i - 1] == s2[j - 1]) {
longestCommonSubsequence = s1[i - 1] + longestCommonSubsequence;
}*/
}
cout << dp[row - 1][col - 1] << endl;
cout << longestCommonSubsequence << endl;
printDP(dp, row, col);
// new了内存空间就要delete;
// 注意这种表达方式;
for (int i = 0; i < row; ++i) {
delete[] dp[i];
}
delete[] dp;
}
return 0;
}
(2019/11/24 21:11 补充)
动态规划实现的最长公共子序列的路径回溯,存在局限性 —— 只能选择边缘路径;
即:至多输出两种可能的最长公共子序列。
除非有人自己在横着走和竖着走都可行的那段代码,采用随机数选择法回溯路径。可是没有必要做这种费力又不讨好的无用功。
而路径回溯只能输出一个最长公共子序列,如果公共序列存在的话。
#include
#include
#include
#include
using namespace std;
typedef vector<vector<int>> VVI;
typedef vector<int> VI;
void outResultVVI(const VVI&);
int main()
{
string s1, s2; // 待输入的两字符串
string longestCommonSubsequence; // 最长相同子序列;
while (cin >> s1 >> s2) {
int row = s1.length() + 1; // 矩阵行数(长度);
int col = s2.size() + 1; // 矩阵列数(宽度);
longestCommonSubsequence.clear(); // 初始化时,需要重置为空;
VVI dp(row, VI(col));
for (int i = 0; i < row; ++i) {
fill(dp[i].begin(), dp[i].end(), 0);
}
//outResultVVI(dp);
for (int i = 0; i < row - 1; ++i) {
for (int j = 0; j < col - 1; ++j) {
dp[i + 1][j + 1] = (s1[i] == s2[j] ? dp[i][j] + 1 : max(dp[i][j + 1], dp[i + 1][j]));
}
}
// 回溯,通过路径拼凑出LCS;
int i = row - 1;
int j = col - 1;
while (i > 0 && j > 0) {
if (i - 1 >= 0 && j - 1 >= 0 && s1[i - 1] == s2[j - 1]) {
longestCommonSubsequence = s1[i - 1] + longestCommonSubsequence;
}
// 位置敏感,若是不先进行判断是否添加字符而是直接回溯,将会遗漏最后一个元素
if (dp[i][j] == dp[i - 1][j - 1] + 1 && s1[i - 1] == s2[j - 1]) {
--i;
--j; // 走斜线(往左上方);
}
else if (dp[i - 1][j] > dp[i][j - 1]) {
--i; // 竖着走(往上);
}
else if (dp[i - 1][j] < dp[i][j - 1]) {
--j; // 横着走(往左);
}
else {
--i;
//--j;
// 横竖都行,往上、往左二选一,选择不同、最长公共子串的结果不同;
}
}
cout << dp[row - 1][col - 1] << endl;
cout << longestCommonSubsequence << endl;
outResultVVI(dp);
}
return 0;
}
void outResultVVI(const VVI& vvi) {
for (int i = 0; i < vvi.size(); ++i) {
for (int j = 0; j < vvi[0].size(); ++j) {
if (j == vvi[0].size() - 1) {
cout << vvi[i][j] << endl;
}
else {
cout << vvi[i][j] << "\t";
}
}
}
}
亲自动手,丰衣足食。
2019/11/22 00:20
============ 我是分割线 ============
只求取最长公共子序列长度时,空间复杂度可从O(mn)降至O(min{m,n}),因为动态规划问题的本质仅仅是考虑:
dp[i][j]该 依据什么,从dp[i-1][j-1]、dp[i-1][j]和dp[i][j-1]三者中做出选择并生成自身数值;
其中:m,n为两字符串长度。
两行数组即可存储dp矩阵,实现动态滚动即可。
#include
using namespace std;
int main()
{
string s_little, s_large; // 待输入的两字符串
while (cin >> s_little >> s_large) {
if (s_little.length() > s_large.size()) { swap(s_little, s_large); }
vector<vector<int>> dp(2, vector<int>(s_little.size() + 1));
for (int i = 1; i <= s_large.size(); ++i) {
for (int j = 1; j <= s_little.length(); ++j) {
dp[i % 2][j] = (s_large[i - 1] == s_little[j - 1] ? dp[(i - 1) % 2][j - 1] + 1 : max(dp[(i - 1) % 2][j], dp[i % 2][j - 1]));
}
}
cout << dp[s_large.size() % 2][s_little.size()] << endl;
}
return 0;
}
#include
#include
#include
#include
using namespace std;
typedef vector<vector<short>> VVI;
typedef vector<short> VI;
void outResultVVI(const VVI&);
void lcs_generator(int i, int j,const string& str,const VVI& path, string& lcm);
int main()
{
string s_little, s_large; // 待输入的两字符串
string longestCommonSubsequence; // 最长公共子序列
while (cin >> s_little >> s_large) {
// 以较短字符串的长决定两阶矩阵的列数(长度);
if (s_little.length() > s_large.size()) {
swap(s_little, s_large);
}
// 矩阵行数(宽度) row = 2;
VVI dp(2, VI()); // 优化dp物理存储空间,二阶矩阵即可,O(min(s1.size(),s2.size()))空间复杂度
for (int i = 0; i < 2; ++i) {
dp[i].resize(s_little.size()+1);
}
VVI path(s_large.length()+1, VI(s_little.size()+1)); // 记录路径,以便回溯,此空间无法优化成滚动数组、数据覆盖、设计不来。。
// 以上为 通过vector创建动态矩阵的两种方式
// 对此涉及目的只有一个,防止访问二阶矩阵dp时产生索引越界,故必须定死了dp的列索引j必须对应小字符串的长度
for (int i = 1; i <= s_large.size(); ++i) {
for (int j = 1; j <= s_little.length(); ++j) {
// dp[i%2][j] = (s_large[i-1] == s_little[j-1] ? dp[(i-1)%2][j-1] + 1 : max(dp[(i-1)%2][j], dp[i%2][j-1]));
// 若无需输出 最长公共子序列 而只是输出最长公共子序列的长度,则上一行地三目运算代码直接搞定
if (s_large[i - 1] == s_little[j - 1]) {
dp[i % 2][j] = dp[(i - 1) % 2][j - 1] + 1;
path[i][j] = 1;
}
else if (dp[(i - 1) % 2][j] > dp[i % 2][j - 1]) { // 条件若是改为 >=,则可能是另外一种回溯结果
dp[i % 2][j] = dp[(i - 1) % 2][j];
path[i][j] = 2;
}
else {
dp[i % 2][j] = dp[i % 2][j - 1];
path[i][j] = 3;
}
}
// outResultVVI(dp);
// outResultVVI(path); // 查看中间演变过程
}
cout << dp[s_large.size()%2][s_little.size()] << endl;
// outResultVVI(dp);
// outResultVVI(path); // 查看最终状态
longestCommonSubsequence.clear();
lcs_generator(s_large.size(), s_little.length(), s_large, path, longestCommonSubsequence);
cout << longestCommonSubsequence << endl;
}
return 0;
}
// 输出动态矩阵
void outResultVVI(const VVI& vvi) {
cout << endl;
for (int i = 0; i < vvi.size(); ++i) {
for (int j = 0; j < vvi[0].size(); ++j) {
if (j == vvi[0].size() - 1) {
cout << vvi[i][j] << endl;
}
else {
cout << vvi[i][j] << "\t";
}
}
}
}
// 动态矩阵的 行数i、列数j、(用i则是i对应的)字符串str、路径矩阵path
void lcs_generator(int i, int j,const string& str,const VVI& path, string& lcm) {
if (!i || !j) { return; }
if (1 == path[i][j]) {
lcm = str[i - 1] + lcm;
lcs_generator(i - 1, j - 1, str, path, lcm);
}
else if (2 == path[i][j]) {
lcs_generator(i - 1, j, str, path, lcm);
}
else {
lcs_generator(i, j - 1, str, path, lcm);
}
}
还是经典的测试样例:
357486782
13456778
两种路径两种结果:
横竖都可以走的时候,横着走:35778(下图中的椭圆)
竖着走:34678(下图中的小方块)
请忽略 path矩阵的第一行和第一列的全0数据;
剩下的,索引对应实现元素的回溯查找即可。
本来只是帮助大一学弟解答最长相同子串;演变成如此文章,岂非我本意。
不过,回过过往学习,还真的是、高度不一样了、理解也就更加深刻了。
经历过的人都会懂得的。
纠错:上图中,自左向右的倒数第二列的椭圆应该往下挪4个元素位。
2019/11/22 19:24
以上纯属个人亲自测试结果,如有错误,可以评论区留言告知。
在此谢过!
转载请注明原文出处,再次感谢。
二进制模拟串实现暴力破解——暴力枚举出(最长)公共子序列
2019/11/24 01:11
详情请看本人另外一篇子博客:
查找N个字符串(环)的最长公共子序列
如需转载,请注明出处!
https://blog.csdn.net/I_love_you_dandan/article/details/103173750
联系方式:[email protected]
欢迎各种友善交流。
2019/11/24 21:00