1. 程式人生 > >動態規劃——最長公共子序列 與 最長公共子串

動態規劃——最長公共子序列 與 最長公共子串

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)0111111
D(0)0111222
C(0)0122222
A(0)1122233
B(0)1223334
A(0)1223344
規則:如果兩個字元相同,該數值取其對角線 + 1,如果不同,取它左邊或者上面相鄰數值的最大值。

程式碼如下:

#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 為例子,表格如下:

ABCBDAB
B0101001
D0000200
C0010000
A1000010
B0201002
A1000010

分析:在填表格時,不再像最長公共子序列一樣:如果兩個字元相同,該位置為其左對角線的值 + 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;
}

分析: