问题描述
从一个序列中删去某些元素可以得到一个子序列,对于序列X
和Y
,如果序列Z
既是X
的子序列又是Y
的子序列,那么称Z
是X
和Y
的公共子序列,长度最长的子序列称为最长公共子序列LCS
.给定两个序列X
和Y
,计算的最长公共子序列的长度,输出一种可行的方案.
分析
对于序列X
和Y
的公共子列来说,如果它们最后一个字母一样,那么它们的LCS
一定是去掉最后一个字母的两个序列的LCS
的长度加一;否则就是X
去掉最后一个字母的序列和序列Y
的LCS
的长度与序列Y
去掉最后一个字母的序列与序列X
的LCS
的长度的最大值,所以对于长度为i
的序列X
和长度为j
的序列Y
,它们的LCS
的长度满足:
然后可以用一个两重循环计算出结果,最后的dp[i][j]
就是长度为i
的序列X
和长度为j
的序列Y
的LCS
的长度,对应的代码如下:
void solve(const string & a, const string& b){
memset(dp,0,sizeof(dp));
for(int i = 1;i <= a.length(); ++i){
for(int j = 1;j <= b.length(); ++j){
if(a[i-1] == b[j-1]){
dp[i][j] = dp[i-1][j-1]+1;
}else{
dp[i][j] = max(dp[i-1][j],dp[i][j-1]);
}
}
}
printf("%lld
", dp[a.length()][b.length()]);
}
路径的打印需要从后向前看,判断dp[i][j]==dp[i-1][j]
和dp[i][j]==dp[i][j-1]
,相等表示此时的LCS
和(X_{i-1},Y_j)或(X_i,Y_{j-1})构成的LCS
长度相同,它们的LCS
应该从这两个中选出;如果dp[i][j]==dp[i-1][j]+1
表明从(X_{i-1},Y_j)到(X_i,Y_i)的LCS
,X[i]==Y[j]
,X[i]
被选中作为LCS
,然后序列X
缩短一个单位,去寻找下一个字符;如果dp[i][j]==dp[i][j-1]+1
说明从(X_i,Y_{j-1})转向(X_i,Y_j),Y[j]
向LCS
贡献了一个字符,此时的X[i]
和Y[j]
是相等的.后面两种情况对应求长度时X[i]==Y[j]
的情况,但是dp[i][j]==dp[i-1][j],dp[i][j]==dp[i][j-1],dp[i][j]==dp[i-1][j]+1,dp[i][j]==dp[i]
总是满足其中的两个,所以最后求出的LCS
一定只是其中的一个可行的方案.
void print_path(const string & a, const string& b){
string ans;
for (int i = a.length(), j = b.length(); i > 0 && j > 0; ){
if( dp[i][j] == dp[i-1][j]){
--i;
}else if( dp[i][j] == dp[i][j-1]){
--j;
}else if(dp[i][j] == dp[i-1][j]+1){
ans += a[i - 1];
--i;
}else{
ans += b[j - 1];
--j;
}
}
reverse(ans.begin(), ans.end());
cout << ans << endl;
}
有一点需要注意的是,这样得到的LCS
序列是一个逆序的,最后要经过一个倒置得到的才是要求的LCS
的序列.
从上面的dp
数组可以看出,坐标i
可以认为表示的是已经处理的序列X
的长度,j
表示已经处理的序列Y
的长度,某一时刻的dp[i][j]
仅仅和三个状态相关,分别是dp[i-1][j-1],dp[i-1][j],dp[i][j-1]
,如果列表来看的话,恰好是正上方正左面和左上角的三个方向,而此前的i-2,i-3,...
很明显则与i
无关了,所以可以对上面的程序稍加改动,得到一个滚动数组表示的:
void LCS(const string & a, const string& b){
ll ans[2][N] = {0};
int pre = 1, cur = 0;
for(int i = 1; i <= a.length(); ++i){
swap(cur, pre);
for (int j = 1; j <= b.length(); ++j){
if( a[i-1] == b[j-1] )
ans[cur][j] = ans[pre][j-1] + 1;
else
ans[cur][j] = max(ans[cur][j-1], ans[pre][j]);
}
}
printf("%lld
", ans[cur][b.length()]);
}
用一个dp[2][N]
进行滚动,一行表示处理序列Y
一轮,一个记录的是上轮的结果,一个是当前的结果,没经过依次,交换cur
和pre
的值,用这次的覆盖上上次的,最后cur
哪行的就是最后依次的结果.
题目链接
题目概述
给出一个序列X
和序列Y
,从序列中按下标严格递增((i_{1}<i_{2}<cdots<i_{k}))找出一个子序列,这个子序列也是序列Y
的子序列,即,(a_{i_k}=b_{j_m},i_{k-1}<i_k, j_{m-1}<j_m),计算这样一个最长的子列的长度.
代码实现
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1105;
ll dp[N][N];
void solve(const string & a, const string& b){
memset(dp,0,sizeof(dp));
for(int i = 1;i <= a.length(); ++i){
for(int j = 1;j <= b.length(); ++j){
if(a[i-1] == b[j-1]){
dp[i][j] = dp[i-1][j-1]+1;
}else{
dp[i][j] = max(dp[i-1][j],dp[i][j-1]);
}
}
}
printf("%lld
", dp[a.length()][b.length()]);
}
void LCS(const string & a, const string& b){
ll ans[2][N] = {0};
int pre = 1, cur = 0;
for(int i = 1; i <= a.length(); ++i){
swap(cur, pre);
for (int j = 1; j <= b.length(); ++j){
if( a[i-1] == b[j-1] )
ans[cur][j] = ans[pre][j-1] + 1;
else
ans[cur][j] = max(ans[cur][j-1], ans[pre][j]);
}
}
printf("%lld
", ans[cur][b.length()]);
}
void print_path(const string & a, const string& b){
string ans;
for (int i = a.length(), j = b.length(); i > 0 && j > 0; ){
if( dp[i][j] == dp[i-1][j]){
--i;
}else if( dp[i][j] == dp[i][j-1]){
--j;
}else if(dp[i][j] == dp[i-1][j]+1){
ans += a[i - 1];
--i;
}else{
ans += b[j - 1];
--j;
}
}
reverse(ans.begin(), ans.end());
cout << ans << endl;
}
int main(int argc, const char** argv) {
string a, b;
while(cin>>a>>b){
// solve(a, b);
// print_path(a, b);
LCS(a, b);
}
return 0;
}