最長公共子序列問題
問題描述
最近適逢秋招,於是刷了一些題。騰訊的某道題完全沒有思路,看到的網上解析遂發覺了這麼個經典的“模板問題”。
輸入: String_1: abcdaf String_2: acbcf
輸出:最長公共子序列(abcf)或長度(4)
思路方法
別的方法就不進行贅述了。
首先根據兩個字符串的長度m,n生成一個(m + 1)x (n + 1)的數組dp,並且令dp[m][0] = 0,dp[0][n] = 0。爲什麼這麼做呢,這裏來解釋一下:
我這裏借鑑了一下B站上某個外國人的做法。我們先這樣做,然後依靠我們人爲的去進行判斷,主要目的是爲了填空。動態規劃是以空間換時間的利器吧,有了上個狀態,經過我們的狀態轉移方程就可以推的下一階段的狀態。在這兒,每次操作的狀態其實就是dp[i][j]的值。好,讓我們進行填空。
1.如上圖所示,我們看一下兩個星星所標記的行和列,我們先不管已經填好的這些值,假定我們已經填好了這些,我們下一步怎麼填問號的內容呢?
簡單的很!自己判斷嘛!子串"abc"和子串"acb"的公共子序列長度是啥啊?“ab"或者"ac"吧?我們先不管究竟是"ab"還還是"ac,先填上長度2”。
2. 就這樣,我手動的全部填完了。
然後呢,我們分析一下有沒有什麼規律?如上圖所示,我們已知結果爲abcf,怎麼得到的呢?或者說能不能用某個規律來說明我是怎麼填好每個空的?(當然我們的大腦比較厲害,反正會填,不會的估計題都沒看明白或者出題的真的是非人類了)。
結論:當兩個子串逐字符進行比較的時候,若發現了相同的字符,則此刻的dp[i][j] = dp[i - 1][j - 1] + 1。除此之外,dp[i][j]就看它上面的值和左邊的值哪個大了,把大的值賦給它。
注意!我們雖然寫的是dp[i][j],但實際比較的是String1[i-1]和String2[j-1]。爲啥呢?看圖吧,當我填dp[i][j] = 1的時候,是因爲我發現了String1[0] = String2[0] = "a"的時候吧?而此時的dp[i][j]中的i和j都等於1。現在多少明白爲什麼定義(m + 1)x(n + 1)了吧?爲什麼預先設定了哪些0?可以理解爲兩個字符串,其中一個若爲空字符串,還有個錘子的公共子序列啊~
3.最後來一波總結~如上圖所示,我們知道dp[i][j]**如何確定了吧!
還是很有意思的吧!下面上我的代碼。
#include <string>
#include <algorithm>
using namespace std;
// 聲明兩個字符串s1,s2
string s1, s2;
int main() {
// 輸入
cin >> s1;
cin >> s2;
// 獲取兩個字符串的長度
int m = s1.size();
int n = s2.size();
// 聲明動態規劃數組
int dp[n+1][m+1];
// 填0 初始化
for(int i = 0; i < n+1; i++){dp[i][0] = 0;}
for(int i = 0; i < m+1; i++){dp[0][i] = 0;}
// 根據設定的規則一次填入dp[i][j],其中i, j >= 1
for(int i = 1; i < n+1; i++){
for(int j = 1; j < m+1; j++){
if(s1[j-1] == s2[i-1]){
dp[i][j] = dp[i-1][j-1] + 1;
}else if(dp[i][j-1] > dp[i-1][j]){
dp[i][j] = dp[i][j-1];
}else{
dp[i][j] = dp[i-1][j];
}
}
}
//打印填好的dp數組
for(int i = 1; i < n+1; i++){
for(int j = 1; j < m+1; j++){
cout << dp[i][j] << " ";
}
cout << endl;
}
// 輸出最長公共子序列的長度,就是二維數組dp的最後一個元素
cout << dp[n][m] << endl;
return 0;
}
輸出結果:
4.如何得到最長公共子串?
添加了一個輔助數組來完成這項工作。這裏用遞歸的方法求解並打印。從dp數組的最後一個值開始,然後根據規則往回找,其實dp[i][j]的值來源就三個吧?要麼dp[i-1][j-1],要麼dp[i][j-1],要麼dp[i-1][j]。所以函數的實現就比較簡單了,每次遞歸按照具體的情況找就行了,下面是我的代碼。
#include <string>
#include <algorithm>
using namespace std;
string s1, s2;
// 定義二維數組用於輔助記錄
int record[1000][1000];
//打印最長公共子串子串
void PrintLCS(int m, int n){
if(n == 0 || m == 0){
return;
}
if(record[n][m] == 1){
PrintLCS(m-1, n-1);
cout << s1[m-1];
}else if(record[n][m] == 2){
PrintLCS(m-1, n);
}else{
PrintLCS(m, n-1);
}
}
int main() {
cin >> s1;
cin >> s2;
int m = s1.size();
int n = s2.size();
int dp[n+1][m+1];
for(int i = 0; i < n+1; i++){dp[i][0] = 0;}
for(int i = 0; i < m+1; i++){dp[0][i] = 0;}
for(int i = 1; i < n+1; i++){
for(int j = 1; j < m+1; j++){
if(s1[j-1] == s2[i-1]){
dp[i][j] = dp[i-1][j-1] + 1;
// 若dp[i][j]的值來源於dp[i-1][j-1]+1,則給同位置的record數組賦值爲1
record[i][j] = 1;
}else if(dp[i][j-1] > dp[i-1][j]){
dp[i][j] = dp[i][j-1];
//若來源於dp[i][j-1],則給同位置的record數組賦值爲2
record[i][j] = 2;
}else{
dp[i][j] = dp[i-1][j];
//若來源於dp[i-1][j],則給同位置的record數組賦值爲3
record[i][j] = 3;
}
}
}
for(int i = 1; i < n+1; i++){
for(int j = 1; j < m+1; j++){
cout << dp[i][j] << " ";
}
cout << endl;
}
cout << dp[n][m] << endl;
PrintLCS(m, n);
return 0;
}
結果如下:
加油! 爲了未來!