动态规划(dp):最长公共子序列和最长上升子序列

最长公共子序列:

给你一个序列X和另一个序列Z,当Z中的所有元素都在X中存在,并且在X中的下标顺序是严格递增的,那么就把Z叫做X的子序列。

例如:Z=<a,b,f,c>是序列X=<a,b,c,f,b,c>的一个子序列,Z中的元素在X中的下标序列为<1,2,4,6>。

现给你两个序列X和Y,请问它们的最长公共子序列的长度是多少?
输入包含多组测试数据。每组输入占一行,为两个字符串,由若干个空格分隔。每个字符串的长度不超过100。
对于每组输入,输出两个字符串的最长公共子序列的长度。

输入样例

abcfbc abfcab

programming contest

abcd mnp

输出样例

4

2

0

这道题的要求是每个字符都要经过检验,但是如果用时间复杂度为O(n^2)的逐一遍历方法不用想都知道会TLE, 所以进一步想到动态规划,可以用记忆化数组保存之前的结果。

当第一个字符串s1的长度为n,第二个字符串s2长度为m时,设二位数组dp[n][m]使得:

dp[i][j]表示s1的前i个字符组成的字符串和s2的前j个字符组成的字符串所构成的最长公共子序列的长度。所以得出dp[][]的递推关系式:

s(i+1)和t(j+1)相等时:

dp[i+1][j+1]=dp[i][j]+1

而s(i+1)和t(j+1)不相等时,有两种选择,一种是从s1中选下一个,看看和ti是否相等,另一种就取

t中的下一个字符看看和si是否相等:然后取两者之中的最大值(都相等/不等也没有关系) 

 dp[i+1][j+1]=max(dp[i][j+1],dp[i+1][j])

有这样的递推关系,时间复杂度可以缩减到O(nm)。下面给出ac代码:

#include<bits/stdc++.h>
int max(int a,int b){
    return a>b?a:b;
}
int main() {
    int dp[101][101];
    char a[100],b[100];
    while(scanf("%s",a)!=EOF){
        getchar();
        scanf("%s",b);
        int n=strlen(a);
        int m=strlen(b);
        for(int i=0;i<n;i++){
            for(int j=0;j<m;j++){
                if(a[i]==b[j]){
                    dp[i+1][j+1]=dp[i][j]+1;//两字符相等的情况
                } else {
                    dp[i+1][j+1]=max(dp[i][j+1],dp[i+1][j]);//不相等则取两种情况更大值
                }
            }
        }
        printf("%d\n",dp[n][m]);
    }
    return 0;
}

这种容易重复计算的题目,关键在于想到充分利用每次的计算结果。下面是一道变式:

最长上升子序列:

对于给定序列,A = a0​,a1​,...,an−1​,找出A中的最长上升子序列(LIS)。一个A中的上升子序列定义为一个序列ai0​,ai1​,...,aik​,满足0≤i0​<i1​<…<ik​<n 并且 ai0​<ai1​<…<aik​。

输入

n
a0​
a1​
a2​
....
an−1​

第一行给出整数n。在之后n行中,给出了A序列的成员值。

输出

在一行中输出A中最长上升子序列的长度

数据范围

  • 1≤n≤100000
  • 0≤ai​≤109

样例输入 

5
5
1
3
2
4

样例输出 

3

聪明的你已经想到了,可以先将输入的a数组升序排序,复制到b数组中,在用上面求的方式求a和b的最长公共子序列,它的长度自然就是a的最长上升子序列。想法很好,但是注意数组的长度:n最大可以到100000.时间复杂度为O(n^2),有没有更快的方法可以将它压缩到O(nlogn)呢。当然是有的:

我们额外创建一个数组b,每时每刻都保证b为非降序数列,每读取一个数至a后,就对b进行相应操作:

  1. 假如读入的数为第一个或它大于b中最大的数时,在b数组末尾加入它
  2. 假如读入的数(这里假设为x)等于b中(y)时,不做操作,因为我们可以想象,在x和y之间还有数可能成为最总上升序列中的一员,这样一来x和y相比就没有用了,保留y
  3. 假如读入的数(为x)小于b中最大值而且b中没有数与它相等时,我们做如下操作:在b中寻找不大于x的最左边(小)的数,用x替换它(用的是二分搜索的寻找办法,时间复杂度O(logn)由此而来)。这个操作的理由在下面用具体的例子解释。

以1,5,6,2,3,4这个序列为例

先读入第一个数为1,执行操作1,放至第一位。

数组b
i012345
b[i]1

读入第二个数5,继续放在数组尾部

数组b
i012345
b[i]15

 读入第三个数6,

数组b
i012345
b[i]156

 读入第三个数2, 重点来了,找到b中不小于2的最左边的数5,用2替换它

数组b
i012345
b[i]126

这样做的理由在于:以5为开头的上升序列{5,6}已被打破,用2重新接着最前面的1依然满足升序,而前面5所做的“成绩“也不会被遗忘,保留5实际上就是保留了曾经最长的升序数列{1,5,6}

的长度,想想看假如输入到此截止,b数组的长度3不就是答案嘛,但是把2放在那个位置的意义在哪里呢,请接着往下看。

读入3,这是3把5替换了。

数组b
i012345
b[i]123

此时,{1,2,3}和{1,5,6}已经一样长了。如果这时再读入一个比较大的数比如10,答案就可以是{1,5,6,10}和{1,2,3,10},都是对的。

只可惜下一个数是4,刚好比3大比5小,这样它可以满足{1,2,3,4}而不能满足{1,5,6,4}

数组b
i012345
b[i]1234

这样就体现出我们用2,3替换5,6的作用了,这其实也是动态规划的记忆化减小运算量的作用,只不过不是用dp[]数组记录运算的值,而是用新建一个数组,通过巧妙地计算使得新数组本身的长度达到记忆的目的。值得注意的是,b数组里面的数并不代表最终的最大升序数组,它本身仅仅起到记忆特定的值的作用,序列1,4,5,2就能说明这一点,以下给出完整代码:

#include<bits/stdc++.h>
int main() {
    long n;
    while(scanf("%ld",&n)!=EOF){
        long a[n+1],b[n+1],t=0,z;
        a[0]=b[0]=-1;
        for(long i=1;i<=n;i++){
            scanf("%ld",&a[i]);
            if(i==1||a[i]>b[t]){//操作1
                b[++t]=a[i];
            }else {//用二分搜索不大于a[i]的值
                for(int l=1,r=t;l<=r;){
                         z=(l+r)/2;
                    if(a[i]<=b[z]&&a[i]>b[z-1]){//替换
                        b[z]=a[i];
                        break;
                    }else if(a[i]>b[z]){
                        l=z+1;
                    }else{
                        r=z-1;
                    }
                }
            }
        }
        printf("%ld\n",t);
    }
    return 0;
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值