最长公共子序列:
给你一个序列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)相等时:
而s(i+1)和t(j+1)不相等时,有两种选择,一种是从s1中选下一个,看看和ti是否相等,另一种就取
t中的下一个字符看看和si是否相等:然后取两者之中的最大值(都相等/不等也没有关系)
有这样的递推关系,时间复杂度可以缩减到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进行相应操作:
- 假如读入的数为第一个或它大于b中最大的数时,在b数组末尾加入它
- 假如读入的数(这里假设为x)等于b中(y)时,不做操作,因为我们可以想象,在x和y之间还有数可能成为最总上升序列中的一员,这样一来x和y相比就没有用了,保留y
- 假如读入的数(为x)小于b中最大值而且b中没有数与它相等时,我们做如下操作:在b中寻找不大于x的最左边(小)的数,用x替换它(用的是二分搜索的寻找办法,时间复杂度O(logn)由此而来)。这个操作的理由在下面用具体的例子解释。
以1,5,6,2,3,4这个序列为例
先读入第一个数为1,执行操作1,放至第一位。
i | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
b[i] | 1 |
读入第二个数5,继续放在数组尾部
i | 0 | 1 | 2 | 3 | 4 | 5 |
b[i] | 1 | 5 |
读入第三个数6,
i | 0 | 1 | 2 | 3 | 4 | 5 |
b[i] | 1 | 5 | 6 |
读入第三个数2, 重点来了,找到b中不小于2的最左边的数5,用2替换它
i | 0 | 1 | 2 | 3 | 4 | 5 |
b[i] | 1 | 2 | 6 |
这样做的理由在于:以5为开头的上升序列{5,6}已被打破,用2重新接着最前面的1依然满足升序,而前面5所做的“成绩“也不会被遗忘,保留5实际上就是保留了曾经最长的升序数列{1,5,6}
的长度,想想看假如输入到此截止,b数组的长度3不就是答案嘛,但是把2放在那个位置的意义在哪里呢,请接着往下看。
读入3,这是3把5替换了。
i | 0 | 1 | 2 | 3 | 4 | 5 |
b[i] | 1 | 2 | 3 |
此时,{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}
i | 0 | 1 | 2 | 3 | 4 | 5 |
b[i] | 1 | 2 | 3 | 4 |
这样就体现出我们用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;
}