272. 最长公共上升子序列(lcis,dp)

首先是lis的状态划分图

然后是lcs

结合lis和lcs两种dp问题的分析方法,我们就可以得出lcis的状态分析图

1.首先上升子序列的分析方法:以某个数字为结尾

2.其次公共子序列的分析方法:有4种状态 00,01,10,11

!!!双关键字的问题一般都以“消元”掉某个关键字来进行降维处理

状态表示

集合:所有以a[1,i]和b[1,j]构成的且以b[j]为结尾的公共上升子序列

属性:max

状态计算(决策集合)

不包含a[i]:那么这个集合的最大值=dp[i-1][j]

包含a[i]:在包含a[i]时,我们发现它有多种状态子集,需要进一步划分

由于需要上升子序列,所以要对集合中的各个元素进行遍历

因为所有以b[k]结尾的公共上升子序列的最大值一定是dp[i][k](简单可证)

所以我们枚举 Σ(1~j)max(dp[i][j],dp[i-1][k]+1)

        子序列只包含b[j]一个数,长度是1;
        子序列的倒数第二个数是b[1]的集合,最大长度是f[i - 1][1] + 1;
        …
        子序列的倒数第二个数是b[j - 1]的集合,最大长度是f[i - 1][j - 1] + 1;

dp[i-1][k]是i-1的原因为:定义出发,dp[i][k]是考虑了a[1,i]和b[1,k],dp[i-1][k]是考虑了a[1,i-1]和b[1,k]

如果采用dp[i][k]会导致a[i]被重复计算了两次,可能b[k]==b[j]

决策集合:dp[i][j]的决策集合,所有以a[1..i-1]和b[1,k]元素构成,且以b[k]结尾的公共上升子序列集合

O(n^3)

#include <iostream>
#include <algorithm>
using namespace std;
const int N=3000+5;
int a[N];
int b[N];
int dp[N][N];
int t[N];
int main()
{
    int n;
    cin>>n;
    for(int i=1;i<=n;i++)
        cin>>a[i];
    for(int i=1;i<=n;i++)
        cin>>b[i];
    int res=0;
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=n;j++)
        {
            dp[i][j]=dp[i-1][j];    //比较包含a[i]和不包含哪个更好

            if(a[i]==b[j])
            {
                for(int k=0;k<j;k++)
                    if(b[k]<b[j])
                        dp[i][j]=max(dp[i-1][k]+1,dp[i][j]);
            }
        }
    }
    for(int i=1;i<=n;i++)
        res=max(res,dp[n][i]);
    cout<<res;
}

O(n^2)优化

如何优化?

1.寻找重复操作

2.寻找性质优化

重复操作:在第三重循环时,我们在寻找以b[j]结尾的lcis时,会有重复操作

寻找以b[j]为结尾时,我们需要遍历一遍前面1~j-1  所有元素

寻找以b[j+1]为结尾时,我们需要遍历一遍前面1~j  所有元素

我们可以注意到 b1~bj-1这一段元素是可以不用再次遍历的,存在重复操作。

对于决策集合来说,遍历到j+1,j进入,集合中的元素只增加不减少

那么引用前缀和的思想,引入一个前缀最大值来维护前面的最长公共上升子序列长度

寻找性质优化:

我们注意到一个值被放入决策集合中时,都是在b[j]==a[j]这个大前提条件下的

if(a[i]==b[j])
{
     for(int k=0;k<j;k++)
        if(b[k]<b[j])    //这里也可以写成b[k]<a[i] 因为a[i]==b[j]
          dp[i][j]=max(dp[i-1][k]+1,dp[i][j]);
}
 

 在第二重循环1~j时,i是固定的,即a[i]也是固定的

由于一个子序列能够放入集合中条件a[i]>b[k],那么我们可以在第二重循环中维护一个前缀最大值

for(int i=1;i<=n;i++)
    {//maxv表示前缀最大值(前面最长的公共上升子序列+1)
        int maxv=1; //最差情况是本身
        for(int j=1;j<=n;j++)
        {
            dp[i][j]=dp[i-1][j];
            if(a[i]==b[j])  //a[i]是公共子序列,更新值
                dp[i][j]=max(dp[i][j],maxv);
            //b[j]被放入j+1的决策集合中,检查
            if(a[i]>b[j])   //a[i]>b[k] 才表示可以放入
                maxv=max(maxv,dp[i-1][j]+1);
        }
    }

 问题:

1.为什么可以用a[i]>b[j]代替

我们要求的是最长公共上升子序列

假设a[i]==b[j],第二重循环时,a[i]会逐渐和1~j-1中所有元素进行比较,如果a[i]>b[k](k表示此时的j),那么等价于b[j]>b[k],如果不存在这个b[j],循环结束后,dp[i][...]都是0,如果存在这个b[j],那么维护的这个前缀最大值可以派上用场

2.为什么maxv和dp[i-1][j]取max,而不是f[i][j]

和dp[i-1][j]取max是继承上一步On^3的思路,f[i][j]表示a[i]已经和b[j]匹配过了,我们maxv维护的是所有包含a[i]且以b[j]为结尾的子序列最大长度,应该是a[i]还没有匹配过。

应是dp[i-1][j]+1        //1表示ai被匹配, 

换成dp[i][j]和maxv能过是因为

如果b[j]==b[j+1],他们不是上升的子序列,不会被重复计数,取max时可以重复,但是思路是错误的,如果取不严格上升子序列,用f[i][j]会出错

#include <iostream>
#include <algorithm>
using namespace std;
const int N=3000+5;
int a[N];
int b[N];
int dp[N][N];
int t[N];
int g[N][N];    //一个决策集合
int main()
{
    int n;
    cin>>n;
    for(int i=1;i<=n;i++)
        cin>>a[i];
    for(int i=1;i<=n;i++)
        cin>>b[i];
    
    int res=0;
    for(int i=1;i<=n;i++)
    {//maxv表示前缀最大值(前面最长的公共上升子序列+1)
        int maxv=1; //最差情况是本身
        for(int j=1;j<=n;j++)
        {
            dp[i][j]=dp[i-1][j];
            if(a[i]==b[j])  //a[i]是公共子序列,更新值
                dp[i][j]=max(dp[i][j],maxv);
            //b[j]被放入j+1的决策集合中,检查
            if(a[i]>b[j])   //a[i]>b[k] 才表示可以放入
                maxv=max(maxv,dp[i-1][j]+1);
        }
    }
    for(int i=1;i<=n;i++)
        res=max(res,dp[n][i]);
    cout<<res;
}

总结

实现转移方程时,要注意观察决策集合的范围随着状态的变化情况,对于决策集合元素只  增加不减少  的情况,可以像本题一样维护一个变量来记录决策集合的当前信息,避免重复扫描,达到降维的作用

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值