定义
在介绍最长公共子序列之前,需要先明白什么是子序列和公共子序列。子序列是在一个给定的序列中删除零个或多个元素之后得到的结果。给定两个序列
X
和
例如,如果
X=<A,B,C,D,E,B,A>,Y=<B,D,E,C,A,B>
,那么序列
<B,C,A>
就是
X
和
特征
在最长公共子序列中,令
X=<x1,x2,⋯,xm
和
Y=<y1,y2,⋯,yn
为两个序列,
Z=<z1,z2,⋯,zk
为
X
和
- 如果 xm=yn ,则 zk=xm=yn 且 Zk−1 是 Xm−1 和 Yn−1 的一个最长公共子序列。
- 如果
xm≠yn
,那么
zk≠xm
意味着
Z
是
Xm−1 和 Y 的一个最长公共子序列。 - 如果
xm≠yn ,那么 zk≠yn 意味着 Z 是Xm 和 Yn−1 的一个最长公共子序列。 - 当其中一个为零时,最长公共子序列为空。
实现
基于上述的特征,可以很容易想到最长公共子序列的递归实现版本。
递归
#include <iostream>
#include <string>
std::string lcs(const std::string& s1, const std::string& s2)
{
size_t len1, len2;
len1 = s1.length();
len2 = s2.length();
if (len1 == 0 || len2 == 0) {
return "";
}
if (s1[len1-1] == s2[len2-1]) {
char tmp = s1[len1-1];
return lcs(s1.substr(0, len1-1), s2.substr(0, len2-1)) + tmp;
}
std::string tmp1 = lcs(s1, std::string(s2, 0, len2-1));
std::string tmp2 = lcs(std::string(s1, 0, len1-1), s2);
return tmp1.length() > tmp2.length() ? tmp1 : tmp2;
}
int main()
{
std::string s1("ABCBDAB");
std::string s2("BDCABA");
std::cout <<lcs(s1, s2) <<std::endl;
return 0;
}
在递归实现中,当 xm≠yn 时,存在子问题重叠的现象,即 lcs(xm,yn−1),lcs(xm−1,yn) 中均包含求解 lcs(xm−1,yn−1) 。针对子问题重叠的问题,一般采用动态规划的解决方案更为有效。
动态规划
#include <iostream>
#include <string>
#include <vector>
enum {
INIT = 0, LEFT, UP, LEFT_UP
};
typedef std::vector<std::vector<int> > Array2D;
std::string get_lcs(const Array2D& direction, const std::string& s, size_t row, size_t col)
{
if (row < 0 || col < 0) {
return "";
}
if (direction[row][col] == LEFT_UP) {
std::string tmp;
if (row > 0 && col > 0) {
tmp = get_lcs(direction, s, row-1, col-1);
}
return tmp + s[row];
} else if (direction[row][col] == LEFT) {
if (col > 0) return get_lcs(direction, s, row, col-1);
} else if (direction[row][col] == UP) {
if (row > 0) return get_lcs(direction, s, row-1, col);
}
return "";
}
std::string dp_lcs(const std::string& s1, const std::string& s2)
{
size_t len1 = s1.length();
size_t len2 = s2.length();
if (len1 == 0 || len2 == 0) {
return "";
}
Array2D lcs_len(len1, std::vector<int>(len2));
Array2D lcs_direction(len1, std::vector<int>(len2));
size_t i, j;
for (i = 0; i < len1; i++) {
for (j = 0; j < len2; j++) {
lcs_len[i][j] = 0;
lcs_direction[i][j] = INIT;
}
}
for (i = 0; i < len1; i++) {
for (j = 0; j < len2; j++) {
if (i == 0 || j == 0) {
if (s1[i] == s2[j]) {
lcs_len[i][j] = 1;
lcs_direction[i][j] = LEFT_UP;
} else {
if (i > 0) {
lcs_len[i][j] = lcs_len[i-1][j];
lcs_direction[i][j] = UP;
}
if (j > 0) {
lcs_len[i][j] = lcs_len[i][j-1];
lcs_direction[i][j] = LEFT;
}
}
} else if (s1[i] == s2[j]) {
lcs_len[i][j] = lcs_len[i-1][j-1]+1;
lcs_direction[i][j] = LEFT_UP;
} else if (lcs_len[i-1][j] >= lcs_len[i][j-1]) {
lcs_len[i][j] = lcs_len[i-1][j];
lcs_direction[i][j] = UP;
} else {
lcs_len[i][j] = lcs_len[i][j-1];
lcs_direction[i][j] = LEFT;
}
}
}
return get_lcs(lcs_direction, s1, len1-1, len2-1);
}
int main()
{
std::string s1("ABCBDAB");
std::string s2("BDCABA");
std::cout <<dp_lcs(s1, s2) <<std::endl;
return 0;
}
在上面的示例代码中,数组lcs_direction
的唯一作用就是恢复最长子序列,但注意到,对于每个lcs_len[i][j]
中的值,只依赖于lcs_len[i-1][j],lcs_len[i][j-1],lcs_len[i-1][j-1]
,因此,我们可以通过lcs_len
表构造出最长子序列,从而节省lcs_direction
所需的空间。修改后的代码如下:
#include <iostream>
#include <string>
#include <vector>
typedef std::vector<std::vector<int> > array2d;
typedef std::vector<int> array1d;
void print_array2d(array2d array)
{
int row = array.size();
int col = array[0].size();
for (int i = 0; i < row; ++i) {
for (int j = 0; j < col; ++j) {
std::cout <<array[i][j] <<" ";
}
std::cout <<std::endl;
}
}
std::string get_lcs(const std::string& s1, const std::string& s2)
{
size_t len1, len2;
len1 = s1.length();
len2 = s2.length();
array2d lcs_len(len1, array1d(len2));
/* initialize the first row and column */
for (int i = 0; i < len1; ++i) {
lcs_len[i][0] = 0;
}
for (int i = 0; i < len2; ++i) {
lcs_len[0][i] = 0;
}
for (int i = 0; i < len1; ++i) {
for (int j = 0; j < len2; ++j) {
if (i == 0 || j == 0) {
if (s1[i] == s2[j]) {
lcs_len[i][j] = 1;
} else {
if (i > 0) {
lcs_len[i][j] = lcs_len[i-1][j];
}
if (j > 0) {
lcs_len[i][j] = lcs_len[i][j-1];
}
}
} else {
if (s1[i] == s2[j]) {
lcs_len[i][j] = lcs_len[i-1][j-1] + 1;
} else {
if (lcs_len[i-1][j] >= lcs_len[i][j-1]) {
lcs_len[i][j] = lcs_len[i-1][j];
} else {
lcs_len[i][j] = lcs_len[i][j-1];
}
}
}
}
}
print_array2d(lcs_len);
len1--, len2--;
std::string lcs_str;
while (len1 >= 0 && len2 >= 0) {
if (lcs_len[len1][len2] == 1) {
lcs_str = s1[len1] + lcs_str;
break;
}
if (lcs_len[len1][len2] > lcs_len[len1-1][len2] && lcs_len[len1][len2] > lcs_len[len1][len2-1]) {
lcs_str = s1[len1] + lcs_str;
len1--, len2--;
} else if (lcs_len[len1-1][len2] >= lcs_len[len1][len2-1]) {
len1--;
} else {
len2--;
}
}
return lcs_str;
}
int main()
{
std::string s1 = "ABCBDAB";
std::string s2 = "BDCABA";
std::cout <<get_lcs(s1, s2) <<std::endl;
return 0;
}
注意
最长公共子序列和最长公共子串的区别。最长公共子序列不要求连续,而最长公共子串需要元素之间保持连续。
参考
[1] 《算法导论》
[2] 最长公共子序列
[3] 动态规划算法解最长公共子序列LCS问题