这个问题很有意思,在生物应用中,经常需要比较两个(或多个)不同生物体的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 = zj。例如,Z = BCDB是ABCBDAB的子序列,对应的下标为[2,3,5,7]。
如果用暴力求解则很容易就达到指数阶的运行时间,所以显然需要优化。通过分析发现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(最长回文子串)等等。
参考资料:
《算法导论》