助你深刻理解——最長公共子串、最長公共子序列(應該是全網數一數二的比較全面的總結了)







最長公共子串篇(20191120)



理論知識:

推薦參考該博文:java實現字符串匹配問題之求兩個字符串的最大公共子串
當然這篇也一樣,看個人理解:求兩個字符串的最長公共子串

圖形理解:


矩陣初始化:

數據初始化

矩陣數值演變:

在這裏插入圖片描述

類似算法:

圖論中的最短路徑算法。
大致分有:迪傑斯特拉算法(Dijkstra)和弗洛伊德算法(Floyd)。
(對應着 貪心算法和動態規劃 …… 別慌,名字起的高大尚並無影響理解。。。)
數據結構算法編程課、離散數學課、計算機網絡課等都會涉及該算法。
本質都是化作矩陣,故線性代數一定要好好學。

最好的理解方式是什麼:
親自動手 —— 圖解,自己手動在草稿紙上推演一遍(小矩陣即可)。

代碼實現(C++):

太長時間沒寫C了,一入python差些找不着回頭路(哈哈)
#include<iostream>
#include<string>
#include<algorithm>
#include<vector>
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<vector<int>>不過建議多多學習、開拓視野
		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])循環進行相應的修改。

最長公共子串:字符一直匹配直到字符不再相同或者已經遍歷完較短字符串;
最長公共子序列:一直遍歷至較短字符串結束即可,當前字符不相同也要繼續匹配下一對字符(各自向後挪動一位)

代碼優化/美化版本補充:

由一道公共子串題目引起的自我反思




============ 我是分割線 ============







最長公共子序列篇(20191121)



理論知識

推薦博客:LCS(最長公共子序列)

講解的很好了,以至於自己發現自己上邊對最長公共子序列的理解過於想當然了。
上邊的理解偏差在於:如何保證是在已有子序列的基礎上去繼續匹配下一對,這纔是子序列的關鍵和難點。

代碼實現與初步理解:

#include<iostream>
#include<string>
#include<algorithm>
#include<vector>
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<vector<int>>不過建議多多學習、開拓視野;
		// 先申請一列,該列的每個元素對應一個一維數組(一行);再每個元素位申請一行。(行、列都僅僅是指一維數組);
		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<iostream>
#include<string>
#include<algorithm>
#include<vector>
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

在這裏插入圖片描述









============ 我是分割線 ============









代碼優化(20191122)


二階滾動數組優化物理存儲空間

代碼優化

只求取最長公共子序列長度時,空間複雜度可從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<bits/stdc++.h>
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<iostream>
#include<string>
#include<vector>
#include<algorithm>
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 個字符串查找最長公共子序列呢?

進一步深入理解:如果是 N 個字符環呢?

詳情請看本人另外一篇子博客:

查找N個字符串(環)的最長公共子序列

如需轉載,請註明出處!
https://blog.csdn.net/I_love_you_dandan/article/details/103173750
聯繫方式:[email protected]
歡迎各種友善交流。
2019/11/24 21:00
發佈了88 篇原創文章 · 獲贊 230 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章