目录
1 最长公共子序列的长度
2 输出所有的最长公共子序列
3 最长公共子串的长度
4 输出所有的最长公共子串
最长公共子串(Longest Common Substring)与 最长公共子序列(Longest Common Subsequence)的区别: 子串要求在原字符串中是连续的,而子序列则只需保持相对顺序,并不要求连续。例如X = {a, Q, 1, 1}; Y = {a, 1, 1, d, f}那么,{a, 1, 1}是X和Y的最长公共子序列,但不是它们的最长公共字串。
一 最长公共子序列
问题描述:给定两个序列:X[1...m]和Y[1...n],求在两个序列中同时出现的最长子序列的长度。
假设 X 和 Y 的序列如下:
X[1...m] = {A, B, C, B, D, A, B}
Y[1...n] = {B, D, C, A, B, A}
可以看出,X 和 Y 的最长公共子序列有 “BDAB”、“BCAB”、“BCBA”,即长度为4。
1) 穷举法
可能很多人会想到用穷举法来解决这个问题,即求出 X 中所有子序列,看 Y 中是否存在该子序列。
X 有多少子序列 —— 2m 个
检查一个子序列是否在 Y 中 —— θ(n)
所以穷举法在最坏情况下的时间复杂度是 θ(n * 2m),也就是说花费的时间是指数级的,这简直太慢了。
2) 动态规划
首先,我们来看看 LCS 问题是否具有动态规划问题的两个特性。
① 最优子结构
设 C[i,j] = |LCS(x[1...i],y[1...j])|,即C[i,j]表示序列X[1...i]和Y[1...j]的最长公共子序列的长度,则 C[m,n] = |LCS(x,y)|就是问题的解。
递归推导式:
从这个递归公式可以看出,问题具有最优子结构性质!
② 重叠子问题
根据上面的递归推导式,可以写出求LCS长度的递归伪代码:
LCS(x,y,i,j)
if x[i] = y[j]
then C[i,j] ← LCS(x,y,i-1,j-1)+1
else C[i,j] ← max{LCS(x,y,i-1,j),LCS(x,y,i,j-1)}
return C[i,j]
C++代码如下:(递归求解)
// 简单的递归求解LCS问题
#include <iostream>
#include <string>
using namespace std;
int max(int a, int b)
{
return (a>b)? a:b;
}
// Return the length of LCS for X[0...m-1] and Y[0...n-1]
int lcs(string &X, string &Y, int m, int n)
{
if (m == 0 || n == 0)
return 0;
if (X[m-1] == Y[n-1])
return lcs(X, Y, m-1, n-1) + 1;
else
return max(lcs(X, Y, m, n-1), lcs(X, Y, m-1, n));
}
int main()
{
string X = "ABCBDAB";
string Y = "BDCABA";
cout << "The length of LCS is " << lcs(X, Y, X.length(), Y.length());
cout << endl;
getchar();
return 0;
}
像这样使用简单的递归,在最坏情况下(X 和 Y 的所有字符都不匹配,即LCS的长度为0)的时间复杂度为 θ(2n)。这和穷举法一样还是指数级的,太慢了。
根据程序中 X 和 Y 的初始值,我们画出部分递归树:
递归树中红框标记的部分被调用了两次。如果画出完整的递归树,我们会看到很多重复的调用,所以这个问题具有重叠子问题的特性。
简单的递归之所以和穷举法一样慢,因为在递归过程中进行了大量的重复调用。而动态规划就是要解决这个问题,通过用一个表来保存子问题的结果,避免重复的计算,以空间换时间。前面我们已经证明,最长公共子序列问题具有动态规划所要求的两个特性,所以 LCS 问题可以用动态规划来求解。
下面是用动态规划(打表)解决LCS问题:
C++代码如下:(动