最长公共子序列问题LCS
-
公共子序列
子序列:给定一个序列X=<x1,x2…,xm>,另一个序列Z=<z1,z2…,zk>,存在一个严格递增的X的下标序列<i1,i2…ik>,满足对所有的j=1,2,…,k,xij = zj
公共子序列:给定两个序列X和Y,Z同时是X和Y的子序列,称Z是X和Y的公共子序列。 -
LCS的最优子结构
令X=<x1,x2…,xm>和Y=<y1,y2,…,ym>为两个序列,Z=<z1,z2…,zk>为X和Y的任意LCS
(1) 如果xm=yn,那么zk=xm=yn且Zk-1是Xm-1Yn-1的一个LCS
(2) 如果xm≠yn,那么zk≠xm意味着Z是Xm-1和Y的一个LCS
(3) 如果xm≠yn,那么zk≠yn意味着Z是X和Yn-1的一个LCS -
递归解
c[i,j]是Xi和Yj的LCS长度
c[i,j]
= 0 , i=0且j=0
= c[i-1,j-1] ,i,j>0且xi=yi
= max(c[i,j-1],c[i-1,j]), i,j>0且xi≠yj -
例子
X:ABCBDAB Y:BDCABA => LCS:BCBA
动态规划法
- 算法
说明:声明了一个m+1*n+1大小的table,0行0列初始化为0,然后从左往右,从上往下地按照递归解填表。填表结束后通过Print可以递归地按同样的方法打印出所求的子序列。
LCS-LENGTH(X,Y)
m=X.length+1
n=Y.length+1
let c[0..m,0..n] be new table
for i=0 to m
c[i,0] = 0
for j=0 to n
c[0,j] = 0
for i=0 to m
for j=1 to n
if x[i]==y[j]
c[i,j]=c[i-1,j-1]+1
else if c[i-1,j]>=c[i,j-1]
c[i,j]=c[i-1,j]
else c[i,j]=c[i,j-1]
return c
PRINT-LCS(c,X,i,j)
if i==0 and j==0
return
if c[i,j]==c[i-1,j-1]+1
PRINT-LCS(c,X,i-1,j-1)
print x[i]
else if c[i,j]=c[i-1,j]
PRINT-LCS(c,X,i-1,j)
else
PRINT-LCS(c,X,i,j-1)
- 截图
- 代码
int LCS_bottom_to_top(vector<vector<int>> & c, const vector<char> & x, const vector<char> & y)
{
int m = x.size()+1;
int n = y.size()+1;
c.resize(m);
for(int i=0;i<m;i++)
{
c[i].resize(n);
}
for(int i=0;i<m;i++)
{
c[i][0] = 0;
}
for(int j=0;j<n;j++)
{
c[0][j] = 0;
}
for(int i=1;i<m;i++)
{
for(int j=1;j<n;j++)
{
if(x[i-1]==y[j-1])
c[i][j] = c[i-1][j-1]+1;
else if(c[i-1][j]>=c[i][j-1])
c[i][j] = c[i-1][j];
else
c[i][j] = c[i][j-1];
}
}
return c[m-1][n-1];
}
void Print_LCS(const vector<vector<int>> & c,const vector<char>&x, const vector<char>&y, int i, int j)
{
if(i==0 && j==0)
return;
if(x[i-1]==y[j-1])
{
Print_LCS(c,x,y,i-1,j-1);
cout<<x[i-1]<<" ";
}
else if(c[i][j]==c[i-1][j])
Print_LCS(c,x,y,i-1,j);
else
Print_LCS(c,x,y,i,j-1);
}
备忘录法
- 算法
解释:
LCS_memorized函数:用于将table初始化,0行0列都为0,其他元素都为-1,表示备忘录没有记录这些元素,初始化完成调用LCS_RECUR函数。
LCS_RECUR函数:如果备忘录已经有记录直接返回,否则递归地查表并回填备忘录的当前位置。
LCS_MEMORIZED(c, X, Y)
m=X.length+1
n=Y.length+1
let c[0..m,0..n] be new table
for i=0 to m
c[i,0] = 0
for j=0 to n
c[0,j] = 0
for i=0 to m
for j=0 to n
c[i,j] = -1
return LCS_RECUR(c,X,Y,m,n)
LCS_RECUR(c,X,Y,i,j)
if c[I,j] ≠ -1
return c[I,j]
if x[i-1]==y[j-1]
lcs = LCS_RECUR(c,x,y,i-1,j-1)+1
else
up = LCS_RECUR(c,x,y,i-1,j)
left = LCS_RECUR(c,x,y,i,j-1)
lcs = max(up, left)
c[i,j] = lcs
return lcs
2.截图
3.代码(包括动态规划法)
#include <iostream>
#include <vector>
#include <iomanip>
using namespace std;
int LCS_recur(vector<vector<int>> & c, const vector<char> & x, const vector<char> & y, int i, int j)
{
if(c[i][j]!=-1)
return c[i][j];
int lcs;
if(x[i-1]==y[j-1])
lcs = LCS_recur(c,x,y,i-1,j-1)+1;
else
{
int up = LCS_recur(c,x,y,i-1,j);
int left = LCS_recur(c,x,y,i,j-1);
lcs = up>=left?up:left;
}
c[i][j] = lcs;
return lcs;
}
int LCS_memorized(vector<vector<int>> & c, const vector<char> & x, const vector<char> & y)
{
int m = x.size()+1;
int n = y.size()+1;
c.resize(m);
for(int i=0;i<m;i++)
{
c[i].resize(n);
}
for(int i=0;i<m;i++)
{
c[i][0] = 0;
}
for(int j=0;j<n;j++)
{
c[0][j] = 0;
}
for(int i=1;i<m;i++)
{
for(int j=1;j<n;j++)
{
c[i][j] = -1;
}
}
return LCS_recur(c,x,y,x.size(),y.size());
}
int LCS_bottom_to_top(vector<vector<int>> & c, const vector<char> & x, const vector<char> & y)
{
int m = x.size()+1;
int n = y.size()+1;
c.resize(m);
for(int i=0;i<m;i++)
{
c[i].resize(n);
}
for(int i=0;i<m;i++)
{
c[i][0] = 0;
}
for(int j=0;j<n;j++)
{
c[0][j] = 0;
}
for(int i=1;i<m;i++)
{
for(int j=1;j<n;j++)
{
if(x[i-1]==y[j-1])
c[i][j] = c[i-1][j-1]+1;
else if(c[i-1][j]>=c[i][j-1])
c[i][j] = c[i-1][j];
else
c[i][j] = c[i][j-1];
}
}
return c[m-1][n-1];
}
void Print_LCS(const vector<vector<int>> & c,const vector<char>&x, const vector<char>&y, int i, int j)
{
if(i==0 && j==0)
return;
if(x[i-1]==y[j-1])
{
Print_LCS(c,x,y,i-1,j-1);
cout<<x[i-1]<<" ";
}
else if(c[i][j]==c[i-1][j])
Print_LCS(c,x,y,i-1,j);
else
Print_LCS(c,x,y,i,j-1);
}
void print(const vector<vector<int>> & c)
{
for(int i=0;i<c.size();i++)
{
for(int j=0;j<c[i].size();j++)
{
cout<<setw(3)<<left<<c[i][j]<<" ";
}
cout<<endl;
}
cout<<endl;
}
void print(const vector<char>&x )
{
for(int i=0;i<x.size();i++)
{
cout<<x[i]<<" ";
}
cout<<endl;
}
int main(void)
{
vector<vector<int>> c;
vector<char> x = {'A','B','C','B','D','A','B'};
vector<char> y = {'B','D','C','A','B','A'};
//LCS_bottom_to_top(c,x,y);
LCS_memorized(c,x,y);
print(x);
print(y);
cout<<endl;
print(c);
cout<<"LCS:";
Print_LCS(c,x,y,x.size(),y.size());
return 0;
}
对比动态规划法与备忘录法
-
方法比较
动态规划法是自底向上,仅通过迭代就可以完成,观察C表的填充顺序,是从上至下从左至右依次填充。当填充完毕后可以得到任意位置(任意子序列)的LCS。
备忘录法是自顶到底,递归地填充C表,仅会填充计算当前LCS会用到的项,所以当执行完毕后还有许多元素尚未填充,意味着只能得到部分子序列的LCS。 -
时间复杂度
两种方法的时间复杂度都是O(mn),因为都需要维护C[m][n]的表。如果更细致地比较备忘录法需要填充的元素较少,填充过程的代价是O(max(m,n)),而动态规划法填充过程的代价是O(mn),但是实际上备忘录法初始化复杂度是O(mn),并且递归调用也会在常数项上增加时间代价。所以两种方法实际的执行速度差别不大。 -
空间复杂度:O(mn)