最长公共子序列(LCS)

最长公共子序列

给定两个序列,找出它们之间最长的公共子序列的长度。注意区分子序列和字串概念。

朴素做法思路:

  • 一种朴素的算法是使用动态规划。首先,定义一个二维数组dp,其中dp[i][j]表示第一个序列的前i个字符与第二个序列的前j个字符的最长公共子序列的长度。为了求解dp[i][j],需要考虑以下两种情况:

    • 如果第一个序列的第i个字符与第二个序列的第j个字符相同,则这两个字符都应该包含在最长公共子序列中,因此可以将它们添加到最长公共子序列中,同时将dp[i][j]更新为dp[i-1][j-1]+1
    • 如果第一个序列的第i个字符与第二个序列的第j个字符不同,则这两个字符至少有一个不包含在最长公共子序列中。因此,可以通过比较dp[i-1][j]dp[i][j-1]来确定dp[i][j]的值。具体地,如果dp[i-1][j]大于等于p[i][j-1],则最长公共子序列包含第一个序列的第i个字符,但不包含第二个序列的第j个字符。反之,如果dp[i-1][j]小于dp[i][j-1],则最长公共子序列包含第二个序列的第j个字符,但不包含第一个序列的第i个字符。如果两者相等,则可以任选其一。
  • 最终,dp[n][m]的值就是最长公共子序列的长度,其中nm分别表示第一个序列和第二个序列的长度。

  • 时间复杂度具体估算方法为状态数量*状态转移,此处一共n*m个状态,每次转移是O(1)的,故时间复杂度为O(nm)

#include<iostream>
#include<algorithm>
using namespace std;
const int N=1010;
int n,m;
int f[N][N];
char a[N],b[N];//这里最好不要用string,因为用char我们可以自己决定读入的位置。方便处理f[i-1][j-1]的情况
int main()
{
    cin>>n>>m>>(a+1)>>(b+1);//字符数组名表示了该数组在内存中的首地址,也就是它的指针,+1表示偏移到字符数组的第二个位置也就是a[1]开始读入数据
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            if(a[i]==b[j]) f[i][j]=f[i-1][j-1]+1;
            else{
                f[i][j]=max(f[i-1][j],f[i][j-1]);
            }
        }
    }
    cout<<f[n][m];
    return 0;
}
最后,诸位不奇怪为什么直接两层for循环嵌套就能如此顺利的求解吗?

我们最开始的思路可是同时考虑ij的,类似于上帝视角,两根指针分别在两个串上滑动,思考起来相当流畅。但当真正开始写的时候就会发现,这个时候两层for循环嵌套其实是一根的指针不动,一根滑倒底的那种。。顿时产生一种怪异感。

但是,想到这是线性DP,又是计算n*m个状态,结合f[N][N]数组,我们不难发现,依据表达式,f[N][N]中的每一项或者说状态都是来自——当前层左侧和上一层的(我画的图太丑了就不放了),恰好符合两层for遍历的顺序。每次更新f[i][j]用到的值都是已经更新过的了。

强烈建议稍微画一下二维表格,随便挑一个格子,按f[i][j]=f[i-1][j-1]+1; f[i][j]=max(f[i-1][j],f[i][j-1]);比划一下就知道了,最好再找两个字串按for循环嵌套的方式更新一下,你就会发现每次需要用到的值都已经更新好了。

总结一下:直接两层for循环嵌套就能如此顺利的求解,是因为状态转移方程表达式决定的,每次更新一个状态,我们都要确保它所需要用到的状态已经在前面更新过了!

如果发现了上面每一次更新只用到了前面一层和当前层左侧的数据,那么理所当然的想到用滚动数组来优化了!
#include<iostream>
#include<algorithm>
using namespace std;
const int N=1010;
int n,m;
int f[2][N];
char a[N],b[N];
int main()
{
    cin>>n>>m>>(a+1)>>(b+1);
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            if(a[i]==b[j]) f[i%2][j]=f[(i-1)%2][j-1]+1;
            else{
                f[i%2][j]=max(f[(i-1)%2][j],f[i%2][j-1]);
            }
        }
    }
    cout<<f[n%2][m];
    return 0;
}

还有些按位与来实现滚动数组的,肯定比求余要快的:

for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            if(a[i]==b[j]) f[i&1][j]=f[(i-1)&1][j-1]+1;
            else{
                f[i&1][j]=max(f[(i-1)&1][j],f[i&1][j-1]);
            }
        }
    }
    cout<<f[n&1][m];
  • 7
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值