最长公共上升子序列

最长公共上升子序列

题目描述

在这里插入图片描述


核心思路

状态表示

  • 集合: d p [ i ] [ j ] dp[i][j] dp[i][j]表示第一个序列的前i个元素(即A[1…i])以及第二个序列的前j个元素(即B[1…j]),并且 b [ j ] b[j] b[j]结尾公共上升子序列的集合。
  • 属性:Max。表示 d p [ i ] [ j ] dp[i][j] dp[i][j]这个集合子序列中长度最大值。

如何划分集合呢?

根据公共子序列中是否包含 a [ i ] a[i] a[i]来划分,实质就是根据 a [ i ] = = b [ j ] a[i]==b[j] a[i]==b[j]来划分。

我们可以先根据 a [ i ] = = b [ j ] a[i]==b[j] a[i]==b[j]成立与否,将这个集合划分为两部分。

  • 如果 a [ i ] ! = b [ j ] a[i]!=b[j] a[i]!=b[j],说明当加入 a [ i ] a[i] a[i] b [ j ] b[j] b[j]后,并不能构成新的公共子序列。那么此时dp值仍然是继承前i-1个元素的,即 d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j]=dp[i-1][j] dp[i][j]=dp[i1][j]
  • 如果 a [ i ] = = b [ j ] a[i]==b[j] a[i]==b[j],说明当加入 a [ i ] a[i] a[i] b [ j ] b[j] b[j]后,可以构成新的公共子序列,那么我们如何求解这一半集合中的dp值呢?划分依据:公共子序列倒数第二个元素在b[]序列中所对应的数
    • 子序列不存在倒数第二个数,只有唯一的一个数 b [ j ] b[j] b[j],由于我们是在 a [ i ] = = b [ j ] a[i]==b[j] a[i]==b[j]的前提下,当 a [ i ] = = b [ j ] a[i]==b[j] a[i]==b[j]时说明这俩可以配对,所以 d p [ i ] [ j ] dp[i][j] dp[i][j]的值至少是1.设为maxv=1。
    • 子序列的倒数第二个数是 b [ 1 ] b[1] b[1],则 d p [ i ] [ j ] = m a x ( m a x v , d p [ i − 1 ] [ 1 ] + 1 ) dp[i][j]=max(maxv,dp[i-1][1]+1) dp[i][j]=max(maxv,dp[i1][1]+1)。为什么要+1呢,因为 d p [ i − 1 ] [ 1 ] dp[i-1][1] dp[i1][1]表示第一个序列A的前i-1个元素以及第二个序列B的前1个元素,并且以 b [ 1 ] b[1] b[1]结尾的公共上升子序列,+1是由于要加上 a [ i ] = = b [ j ] a[i]==b[j] a[i]==b[j]这一对。
    • 子序列的倒数第二个数是 b [ 2 ] b[2] b[2],则 d p [ i ] [ j ] = m a x ( m a x v , d p [ i − 1 ] [ 2 ] + 1 ) dp[i][j]=max(maxv,dp[i-1][2]+1) dp[i][j]=max(maxv,dp[i1][2]+1).
    • $\cdots $
    • 子序列的倒数第二个数是 b [ j − 1 ] b[j-1] b[j1],则 d p [ i ] [ j ] = m a x ( m a x v , d p [ i − 1 ] [ j − 1 ] + 1 ) dp[i][j]=max(maxv,dp[i-1][j-1]+1) dp[i][j]=max(maxv,dp[i1][j1]+1).

现在当 a [ i ] = = b [ j ] a[i]==b[j] a[i]==b[j]时,我们已经对这一半集合中的公共子序列进行了细划分了,但是还要保持上升子序列的性质。为此,需要当 b [ k ] < b [ j ] b[k]<b[j] b[k]<b[j]时才能构成上升子序列,其中 k = 1 , 2 , ⋯   , j − 1 k=1,2,\cdots ,j-1 k=1,2,,j1

在这里插入图片描述

如何理解优化代码呢?

然后我们发现每次循环求得的maxv是满足a[i] > b[k]的 f [ i − 1 ] [ j ] + 1 f[i-1][j]+1 f[i1][j]+1的前缀最大值。因此可以直接将maxv提到第一层循环外面,减少重复计算,此时只剩下两重循环。我们发现只有当 a [ i ] = = b [ j ] a[i]==b[j] a[i]==b[j]时, d p [ i ] [ j ] dp[i][j] dp[i][j]才会被更新,那么 b [ k ] < b [ j ] b[k]<b[j] b[k]<b[j]可以替换为 b [ k ] < a [ i ] b[k]<a[i] b[k]<a[i]。观察k循环这一层,其实就是处理了一个关于 d p [ i ] [ j ] dp[i][j] dp[i][j]前缀的问题,即是在求 b [ 1 ] b[1] b[1] b [ j ] b[j] b[j]中满足 b [ k ] < a [ i ] b[k]<a[i] b[k]<a[i]条件下最大的dp值。真正被用到的也只有那个最大的dp值而已啦。所以我们只需要用一个变量来存储这个前缀并在符合条件 b [ k ] < a [ i ] b[k]<a[i] b[k]<a[i]时的最大dp值即可。

在这里插入图片描述


代码

未优化版

#include<iostream>
#include<algorithm>
using namespace std;
const int N=3010;
int n;
int a[N],b[N];
int dp[N][N];
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    scanf("%d",&a[i]);
    for(int i=1;i<=n;i++)
    scanf("%d",&b[i]);
    //第一个循环控制第一个序列a[i]
    for(int i=1;i<=n;i++)
    {
        //第二个循环控制第二个序列b[j]
        for(int j=1;j<=n;j++)
        {
            //根据是否包含a[i]来将集合划分为两部分,实质划分依据就是:a[i]==b[j]与否
            //如果a[i]!=b[j],说明不包含a[i],那么就继承a[i-1]的dp值
            if(a[i]!=b[j])
            {
                dp[i][j]=dp[i-1][j];
            }
            //如果a[i]==b[j],说明包含a[i],那么需要根据公共子序列倒数第二个元素在b[]序列中所对应的数来将这一部分集合继续划分
            //求出这一部分集合中的每一类情况的最大值maxv,最终就可以求解出这一部分集合的最大值maxv了
            else
            {
                int maxv=1;
                //判断b[1..j-1]是否<b[j],即我们现在是在公共子序列的基础上,要保证是上升子序列,因此需要b[k]<b[j]
                for(int k=1;k<j;k++)
                {
                    if(b[k]<b[j])
                    maxv=max(maxv,dp[i-1][k]+1);//求解出这一部分集合中的每一类情况的最大值
                }
                dp[i][j]=max(dp[i][j],maxv);//比较得出这两部分集合的最大值
            }
        }
    }
    int ans=0;//最长公共上升子序列的长度
    //dp[n][i]表示第一个序列的前n个元素以及第二个元素的前j个元素,并且以b[j]结尾的公共上升子序列集合
    //那么我们需要比较比较枚举是以哪一个b[j]结尾(b[1],b[2],...,b[j]),可以得到最大的公共上升子序列。
    for(int j=1;j<=n;j++)
    ans=max(ans,dp[n][j]);
    printf("%d\n",ans);
    return 0;
}

优化版

#include<iostream>
#include<algorithm>
using namespace std;
const int N=3010;
int n;
int a[N],b[N];
int dp[N][N];
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    scanf("%d",&a[i]);
    for(int i=1;i<=n;i++)
    scanf("%d",&b[i]);
    for(int i=1;i<=n;i++)
    {
        int maxv=1;//记录前缀中最大的dp值
        for(int j=1;j<=n;j++)
        {
            //将一个大集合划分为了两部分
            //这里求出了当a[i]!=b[j]时,一部分集合的最大值
            if(a[i]!=b[j])
            {
                dp[i][j]=dp[i-1][j];
            }
            //这里求出了当a[i]==b[j]时,另一部分的最大值
            else
            {
                //dp[i][j]是求出的一部分集合的最大值,maxv是求出的另一部分集合的最大值
                //最终取这两部分集合中的最大的那个最大值,作为整个集合最终的最大值
                dp[i][j]=max(dp[i][j],maxv);
            }
            //当b[j]<a[i]时,说明这些b[j]被打上了绿勾,我们就求出这些绿勾中所对应的最大的那个dp值,并用maxv来记录
            //比如原来j=6,有b1,b2,b3,b4,b5,假设b2,b3被打上了绿色勾,我们找到了这些绿勾中最大的dp值是b3所对应的dp[i-1][3]
            //因此maxv此时记录的就是b1,b2,b3,b4,b5中被打上绿勾中的最大dp值,即为dp[i-1][3]
            //然后j增大1,即此时j=7,有b1,b2,b3,b4,b5,b6,假设b6也被打上了绿勾,那么我们此时只需要比较前j=5中的最大dp值和b6所对应的dp值dp[i-1][6]+1即可
            if(b[j]<a[i])
            {
                maxv=max(maxv,dp[i-1][j]+1);
            }
        }
    }
    int ans=0;
    for(int j=1;j<=n;j++)
    ans=max(ans,dp[n][j]);
    printf("%d\n",ans);
    return 0;
}

优化为一维

我们不难发现, d p [ i ] [ j ] dp[i][j] dp[i][j]的状态都是来源于上一层,所以可以去掉第一维做个等价变形。即可以只用一维dp。

#include<iostream>
#include<algorithm>
using namespace std;
const int N=3010;
int n;
int a[N],b[N];
int dp[N];
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    scanf("%d",&a[i]);
    for(int i=1;i<=n;i++)
    scanf("%d",&b[i]);
    for(int i=1;i<=n;i++)
    {
        int maxv=1;
        for(int j=1;j<=n;j++)
        {
            // if(a[i]!=b[j])
            // dp[j]=dp[j];
            if(a[i]==b[j])
            dp[j]=max(dp[j],maxv);
            if(b[j]<a[i])
            maxv=max(maxv,dp[j]+1);
        }
    }
    int ans=0;
    for(int i=1;i<=n;i++)
    ans=max(ans,dp[i]);
    printf("%d\n",ans);
    return 0;
}

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卷心菜不卷Iris

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值