動態規劃——最長公共子序列 與 最長公共子串
1、最長公共子序列
LCS 問題,即最長公共子序列問題。它並不要求所求得的字元在所給定的字串中是連續的。比如輸入的兩個字串是 ABCBDAB 和 BDCABA,那麼,BCBA 和 BDAB 都是他們最長的公共子序列。則輸出它們的長度 4。
假設兩個字串 A = [A0,A1....Am],,B = [B0,B1...Bn] 的最長公共子序列是 C = [C0,C1.....Ck]。下面分三種情況進行討論:
(1)如果 Am =Bn,肯定有 Ck=Am =Bn,,也就是說,它們最後一個字元是相同的,換言之,[A0,A1....Am-1] 與 [B0,B1...Bn-1] 的公共子序列長度 + 1 = [A0,A1....Am] 與 [B0,B1...Bn] 的公共子序列長度 。問題轉換為:
LCS(Am,Bn) = LCS(Am-1,Bn-1) + 1
(2)如果 Am !=Bn,且 Ck != Am,就是說,A 和 B 的最後一個字元不相同,但是呢,它們的公共子序列最後一個字元和 A 的最後一個字元不相等,那麼,[C0,C1.....Ck] 也是 [A0,A1....Am-1] 與 [B0,B1...Bn] 的公共子序列.
(3)如果 Am !=Bn,且 Ck != Bm,就是說,A 和 B 的最後一個字元不相同,但是呢,它們的公共子序列最後一個字元和 B 的最後一個字元不相等,那麼,[C0,C1.....Ck] 也是 [A0,A1....Am] 與 [B0,B1...Bn-1] 的公共子序列.
由(2)和(3)可得,如果 Am !=Bn,LCS(Am,Bn) = max { LCS(Am-1,Bn),LCS(Am,Bn-1) }
這裡可以利用填表法進行描述,填表規則為:
if i = 0 or j = 0,則 c[i,j] = 0
if i,j>0 and ai = bj,則c[i,j] = c[i-1,j-1] + 1
if i,j >0 and ai != bj,則c[i,j] = max{c[i,j-1] ,c[i-1,j] }
表格如下:
(0) | A(0) | B(0) | C (0) | B(0) | D(0) | A(0) | B(0) |
B(0) | 0 | 1 | 1 | 1 | 1 | 1 | 1 |
D(0) | 0 | 1 | 1 | 1 | 2 | 2 | 2 |
C(0) | 0 | 1 | 2 | 2 | 2 | 2 | 2 |
A(0) | 1 | 1 | 2 | 2 | 2 | 3 | 3 |
B(0) | 1 | 2 | 2 | 3 | 3 | 3 | 4 |
A(0) | 1 | 2 | 2 | 3 | 3 | 4 | 4 |
程式碼如下:
#include<iostream>
#include<algorithm>
#include<math.h>
using namespace std;
int a[1000][1000] = { 0 };
int long_lcs(const char* X, const char* Y)
{
if (X == NULL || Y == NULL)
return 0;
int m = strlen(X); // m 表示行數
int n = strlen(Y); // n 表示 列數
//int a[1000][1000] = { 0 };
for (int i = 1; i < m; i++)
{
a[i][0] = 0; // 第 0 列 先把第 0 列置為 0
}
for (int j = 1; j < n; j++)
{
a[0][j] = 0; // 第 0 行
}
a[0][0] = 0;
for (int i = 1; i <= m; i++)
{
for (int j = 1; j <= n; j++)
{
if (X[i] == Y[j])
a[i][j] = a[i - 1][j - 1] + 1;
else
a[i][j] = max(a[i-1][j],a[i][j-1]);
}
}
return a[m][n];
}
int main()
{
const char* x = "ABCBDAB";
const char* y = "BDCABA";
//int num = long_lcs(x,y);
int num = long_lcs(x, y);
std::cout << num << endl; // 輸出為 4
return 0;
}
2、最長公共子串
2、1求最長公共子串的長度
與公共子序列不同,公共子串是連續的。那麼,在動態規劃中,每一次碰到新的不同的兩個字元時,計數器都要被置為 0.。還是以字串 ABCBDAB 和 BDCABA 為例子,表格如下:
A | B | C | B | D | A | B | |
B | 0 | 1 | 0 | 1 | 0 | 0 | 1 |
D | 0 | 0 | 0 | 0 | 2 | 0 | 0 |
C | 0 | 0 | 1 | 0 | 0 | 0 | 0 |
A | 1 | 0 | 0 | 0 | 0 | 1 | 0 |
B | 0 | 2 | 0 | 1 | 0 | 0 | 2 |
A | 1 | 0 | 0 | 0 | 0 | 1 | 0 |
分析:在填表格時,不再像最長公共子序列一樣:如果兩個字元相同,該位置為其左對角線的值 + 1,如果不同,則為 max{左邊相鄰的值,上面相鄰的值}。因為公共子串要求連續,那麼,
if (兩個字元相同,即可a[i] == b[j])
{
if (a[i-1] == b[j-1])
該位置的值 = 其左對角線的值 + 1
else
該位置的值 = 1
}
利用狀態轉移方程表示就是:
程式碼如下:
class LongestSubstring {
public:
int findLongest(string A, int n, string B, int m) {
// write code here
int dp[1000][1000] = {0};
int max_len = 0;
for(int i = 0;i < n;i++)
{
for(int j = 0;j < m;j++)
{
if(A[i] == B[j])
{
if(i >0 && j>0)
{
dp[i][j] = dp[i-1][j-1] +1;
}
else
{
dp[i][j] = 1; // i= j= 0 的時候,不能使用上面那個公式,所以要單獨寫出來
}
}
if(max_len < dp[i][j])
max_len = dp[i][j];
}
}
// cout << max_len ;
return max_len ;
}
};
2、2求最長公共子串第一次出現在短的字串中的子串
這個問題不再是求最長公共子串的長度,而是求具體的子串。難度又升級了一下:我們不僅要分析什麼時候取最大的長度,還要確定是第一次出現。第一次最大長度的字元出現,我們要做一個標記,但是我們仍要繼續遍歷剩餘的字元,只不過這個標定不再改變。
程式碼如下:
void findMaxCommonStr(string s1, string s2)
{
if (s1.length()>s2.length())
swap(s1, s2);//s1用於儲存較短的子串
int len1 = s1.length(), len2 = s2.length();
int maxLen = 0, start = 0;
vector<vector<int> >dp(len1 + 1, vector<int>(len2 + 1, 0));
for (int i = 1; i <= len1; ++i)
for (int j = 1; j <= len2; ++j)
{
if (s1[i - 1] == s2[j - 1])
{
dp[i][j] = dp[i - 1][j - 1] + 1;
if (dp[i][j]>maxLen) //此 if() 寫在if (s1[i - 1] == s2[j - 1]) 裡面,能保證是第一次出現的,並且保證取到最大長度
{
maxLen = dp[i][j];
start = i - maxLen;//記錄最長公共子串的起始位置,並且記錄的只有第一次出現 maxLen 的位置。因為如果以後出現相等的最大的,不會進行交換
}
}
}
cout << s1.substr(start, maxLen) << endl;
}
int main()
{
string s1, s2;
while (cin >> s1 >> s2)
{
findMaxCommonStr(s1, s2);
}
return 0;
}
還有另外一種方法,利用 STL 。string 中有一些非常好用的函式,參見我的另外一篇部落格:string 的用法——進階篇 利用 substr() 和 find() 函式,能較方便地解決這個問題:
程式碼如下:
int main()
{
string a, b;
while (cin >> a >> b)
{
if (a.size() > b.size())
swap(a, b);//確保前面的一個字串短;
string str_m;
for (int i = 0; i<a.size(); i++)
{
for (int j = i; j<a.size(); j++)
{
string temp = a.substr(i, j - i + 1);
if (int(b.find(temp))<0) // 確保是第一次找到,且是連續的。如果出現不一樣的字元了,即未找到,就輸出 -1
break;
else if (str_m.size()<temp.size()) // 保證找到的是最長的連續子串
str_m = temp;
}
}
cout << str_m << endl;
}
return 0;
}
分析: