LCS(最長公共子序列)
這個問題很有意思,在生物應用中,經常需要比較兩個(或多個)不同生物體的DNA片段。例如,某種生物的DNA可能為S1 = ACCGGTCGAGTGCGCGGAAGCCGGCCGAA,S2 = GTCGTTCGGAATGCCGTTGCTCTGTAAA。比較兩個DNA串是想要確定它們之間的“相識度”,作為度量兩種生物相近程度的指標。LCS就是尋找第三個串S3,S3滿足定義:給定一個序列X = <x1,x2,...,xn>,另一個序列Y = <y1,y2,...ym>即存在一個嚴格遞增的X的下標序列<i1,i2,...,im>,對所有j = 1,2,...,m,滿足xij
如果用暴力求解則很容易就達到指數階的運行時間,所以顯然需要優化。通過分析發現LCS問題具有最優子結構性質,於是,考慮用動態規劃是一個非常不錯的方案。
顯然早就有許多人對此類問題做過很多工作,不管是理論上還是實際測試,都證明動態規劃可以解決這一類型的問題,有時候甚至是最優方案。如何熟練使用動態規劃的一大難點在於分析問題,要理解如何使用動態規劃來解決問題,首先分析問題是否具有最優子結構是重要的,然後就是通過分析問題得到遞歸公式,只要得到了遞歸公式,我們就可以使用動態規劃了。
LCS問題也不例外,經過分析和查資料,我們知道了LCS得最優子結構定理:
令X = <x1,x2,...,xn>和Y = <y1,y2,...ym>為兩個序列,Z = <z1,z2,...,zk>為X和Y的任意LCS。
- 如果xn = ym,則zk = xn = ym且Zk - 1是Xn - 1和Ym - 1的一個LCS;
- 如果xn ≠ ym且zk ≠ xn,說明Z是Xn - 1和Y的一個LCS;
- 如果xn ≠ ym且zk ≠ ym,說明Z是X和Ym的一個LCS;
然後,我們開始建立最優解的遞歸式。定義dp[i][j]為Xi和Yj的LCS長度。顯然的,當i = 0或j = 0時,LCS = 0。再結合最優子結構定理,可得到遞歸式:
( 0; 當 i = 0 或 j = 0; dp[i][j] = { dp[i - 1][j - 1] + 1 當 i,j > 0 且 xi = yj; ( max(dp[i][j - 1], dp[i - 1][j]) 當 i,j > 0 且 xi != yj.
得到遞歸式後,就可以根據公式寫出遞歸算法或動態規劃算法:
#include <iostream> #include <string> #include <vector> #include <minmax.h> class Solution { public: int LongestCommonSubsequence(std::string s1, std::string s2) { if (s1.empty() || s2.empty()) return 0; std::vector<std::vector<int> > dp(s1.size(), std::vector<int>(s2.size(), 0)); int length = 0; for (int i = 1; i < dp.size(); i++) { int j = 1; for (; j < dp[0].size(); j++) { if (s1[i] == s2[j]) dp[i][j] = dp[i - 1][j - 1] + 1; else dp[i][j] = max(dp[i][j - 1], dp[i - 1][j]); length = max(length, dp[i][j]); } } return length + 1; } }; int main() { Solution solve; std::string s1{"ABCBDAB"}; std::string s2{"ABDCABA"}; std::cout << solve.LongestCommonSubsequence(s1, s2) << std::endl; return 0; }
最後,類似的問題還有LIS(最長遞增子序列)、LPS(最長回文子串)等等。
參考資料:
《算法導論》
LCS(最長公共子序列)