最长公共子序列问题就是从a set of sequences 中找出the longest common subseqence。 通常是just 2 sequence。
注意, 一个subsequence 和一个substring 是不同的。 因为subsequence 并不要求去元素的时候consecutive(连续), 只要是从左往右的取元素即可。, 然而substring 却要求是连续的取。
LCS 问题是计算机领域的一个经典的问题, LCS问题是the basis of file comparision programs such as diff, and has applications in bioinformatics。例如生物学上比较两种生物的DNA的相似性, 就可以用LCS问题建模。
对于任意数量的input sequences的情况下, LCS问题是NP-Hard的问题。 但是当sequence的数量是常数的时候, 这个问题就可以采用dynamic programming(动态规划)在polynomial time的时间内解决。
下面举一个例子:
如下的一个sequence:
所谓的两个strings的common subsequence 就是这两个string中均出现的subsequence。 所谓的最常公共子序列(LCS)就是最长的common subsequence。
例如:
下面两个string:
我们经过分析:
所以这两个string的 LCS为:
回归到算法问题。
一个naive的解决办法就是列举出s1 的所有subsequence, 然后检查其是否也是s2 的subsequences。 不难看出, 这个算法的时间复杂度太大了。
然而, 经过稍微的分析, 我们不难看出LCS问题具有optimal structures。 即这个LCS problem 可以break down 为smaller, simple subproblems, 然后这个subproblem 又可以break down 为simpler subproblem, 直至最终, the solution becomes trivial。
LCS 还具有 overlapping subproblems. : the solution to a higher subproblem depends on the solutions to several of the lower subproblems.
当一个问题optimal substructures (最优子结构) 和 overlapping subproblem 的问题可以使用一种被称为dynamic programming (动态规划)的 problem solving techniques.
NOTE: in cs, a problem is said to have optimal substructure if an optimal solution can be constructed efficiently from optimal solutions of its subproblems.
这个解决问题的过程需要memoization, 用于用表列的形式记录下子问题的解, 从而避免重复计算。
不再证明。
对于LCS(最长公共子序列)的长度,计算, 也有如下公式:
上述算法的伪代码如下:
程序如下:
#include
#include
#include
#include
#include
using std::cout;
using std::endl;
using std::vector;
using std::string;
using std::pair;
int lcs(const string &s1, const string &s2, int m, int n) {
if (m < 0 || n <0) return 0;
if (s1[m] == s2[n]) return 1 + lcs(s1, s2, m-1, n-1);
else return std::max(lcs(s1, s2, m - 1, n), lcs(s1, s2, m, n - 1));
}
int lookUp(const vector> &t, int i, int j) {
if (i < 0 || j < 0) return 0;
else return t[i][j];
}
pair lcsDP(const string &s1, const string &s2, int m, int n) {
vector> t(s1.length(), vector (s2.length()));
for (int i = 0; i < s1.length(); ++i) {
for (int j = 0; j < s2.length(); ++j) {
if (s1[i] ==s2[j]) t[i][j] = lookUp(t, i - 1, j - 1) + 1;
else t[i][j] = std::max(lookUp(t, i - 1, j),
lookUp(t, i, j - 1));
}
}
int i = s1.length() - 1, j = s2.length() - 1;
string result = "";
while(i >= 0 || j >= 0) {
if (s1[i] == s2[j]) {
result.push_back(s1[i]);
--i;
--j;
} else {
if (lookUp(t, i - 1, j) > lookUp(t, i, j -1)) {
--i;
} else {
--j;
}
}
}
std::reverse(result.begin(), result.end());
return pair (t[s1.length() -1][s2.length() - 1], result);
}
int main()
{
string s1 = "aaaabbcbcbc";
string s2 = "aasdjkahgfadfbfgsgebsbsbbc";
auto p = lcsDP(s1, s2, s1.length() - 1, s2.length() - 1);
cout << p.first << " " << p.second << endl;
return 0;
}
编译后, 出现Compiler warning如下:
对于初学者, 不管警告, 运行后如下:
虽然Compiler 警告不会影响程序的执行, 上述的错误是在if语句中, 出现了comparision between signed and unsigned integer expressions。
但是我们说, 这是对于初学者的, 对于开发人员, 必须学会将编译器中的警告 当做出现了错误, 并将这个错误剔除出去。
为什么出现上述错误呢?
原因是string的成员函数length()返回的数据类型是unsigned int。 而i 的数据类型是int(signed, 有符号的), 于是我们作如下修改:
注意对于code::blocks, CTR + 滚动鼠标滑条 可以使得code::blocks 的界面按比例变大变小。
现在编译通过了:
运行得到如下结果: