897. 最长公共子序列
题目描述
给定两个长度分别为N和M的字符串A和B,求既是A的子序列又是B的子序列的字符串长度最长是多少。
输入格式
第一行包含两个整数N和M。
第二行包含一个长度为N的字符串,表示字符串A。
第三行包含一个长度为M的字符串,表示字符串B。
字符串均由小写字母构成。
输出格式
输出一个整数,表示最大长度。
数据范围
1≤N,M≤1000
输入样例:
4 5
acbd
abedc
输出样例:
3
代码实现
思路一
集合表示:f[ i,j ] 表示 a 的前 i 个字符,和 b 的前 j 个字符的最长公共子序列长度。(两个前缀子串的最长公共子序列)
集合划分:以a[i]、b[j]是否包含在子序列当中为依据,因此可以分成四类:
① a[i]不在,b[j]不在:f [i−1,j−1]
② a[i] 不在,b[j]在:f[i−1,j]
看似是f[i−1,j] , 实际上无法用f[i−1,j]表示。因为f[i−1,j]表示的是在a的前i-1个字母和b的前j个字母的最长公共子序列长度,但是这个满足条件f[i-1,j]的最大子序列却不一定包括b[j],而我们要求的是a[i]不在子序列中且b[j]在子序列中的情况,求它的长度。这二者不完全相等。
但仍可以用f[i−1,j]来表示,原因就在于a[i]不在子序列中且b[j]在子序列中的情况被包含在f[i−1,j]中,即该情况是 f[i−1,j]情况的子集,其最大值小于等于f[i -1,j]的最大值,另一个子集是a[i]不在且b[j]也不在子序列中的情况f [i−1,j−1]
,二者合并起来就是全集f[i-1,j]
。
虽然f[i - 1,j]的最大值不一定是 子序列包含b[j] 这种情况,但对答案是没影响的,因为f[i-1,j]中的方案一定是f[i,j]中的方案,只要包含了选b[j]这种情况就行。
f [ i − 1 , j ] 的 值 可 以 分 为 两 部 分 构 成 : 不 选 b [ j ] 和 选 b [ j ] , 就 是 m a x ( f [ i − 1 , j − 1 ] , 选 b [ j ] 的 情 况 ) f[i-1,j]的值可以分为两部分构成:不选b[j]和选b[j],就是 max(f[i-1,j-1],选b[j]的情况) f[i−1,j]的值可以分为两部分构成:不选b[j]和选b[j],就是max(f[i−1,j−1],选b[j]的情况)
例如:要求a,b,c的最大值可以这样求:max( max(a,b) , max(b,c) ) 虽然b被重复使用,但仍能求出max,求max只要保证不漏即可。
③ a[i]在,b[j]不在 :f[i,j-1],原理同②
④ a[i]在,b[j]在: f[i−1,j−1]+1; 前提条件:a[i]==b[j]
因为在计算时,①已经被包含在②和③的情况中,所以①不用考虑了。且情况②和情况③是必然发生的。
即,对于二维表f,每次求f[i,j]时,最多用到三个方向,左f[i-1,j],上f[i,j-1]和左上方f[i-1,j-1],左和上必然会用到,左上方用到需要有前提条件:a[i]==b[j]
初始化情况:
由于涉及i-1、j-1,所以f数组最好从下标1开始存储,因此相关的字符串也从下标1开始存储比较好。
从第1个字符开始处理,所以需要先预处理’第0个字符‘的情况:第0行和第0列都应该初始化为0。
即任何字符串和null字符串匹配都是0。
i=1时,要用到每一列的f[0,j];j=1时,要用到每一行的f[i,0];
i=1、j=1且a[i]=b[j]时,要用到f[0,0]
#include <iostream>
#define read(x) scanf("%d",&x)
using namespace std;
const int N=1010;
char a[N],b[N];
int f[N][N];
int main()
{
int n,m;
read(n),read(m);
scanf("%s%s",a+1,b+1);
for (int i=1;i<=n;i++)
for (int j=1;j<=m;j++) {
f[i][j]=max(f[i-1][j],f[i][j-1]);
if (a[i]==b[j]) f[i][j]=max(f[i][j],f[i-1][j-1]+1);
}
printf("%d",f[n][m]);
return 0;
}
由于求完f[i,j]后,它还会被后面的用到,作为左、上、左上方三个方向的方向被用三次,所以不太可能仅用4个变量代表左、上、左上和答案去优化空间复杂度。
思路二
思路2和思路1看着差不多,但是实际思考方式还是不太一样,且代码的含义也不一样。
思考一个问题:当a[i]==b[j]时,我们是一定要把他们匹配,作为公共子序列的一部分吗?
下面的思路都是在此条件成立的基础上进行的。
按两个序列末尾的字符是不是相等来区分。
如果两个字符相等,就可以直接转移到f[i-1,j-1];
不相等的话,两个字符一定有一个可以抛弃,可以对f[i-1,j],f[i,j-1]两种状态取max来转移。
#include <iostream>
using namespace std;
const int N = 1010;
char a[N], b[N];
int f[N][N];
int main() {
int n, m;
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][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;
}