LCS(longest common sub-sequences)
:最长公共子序列
子串: 按原顺序依次出现,禁止跳过某元素的序列,具有连续性
子序列: 在保持元素前后关系的前提下,可以跳过某些元素的序列,不连续性
密切相关:[线性dp] aw895最长上升子序列(知识理解+重要模板题+最长上升子序列模型+LCS转化LIS) 详看下方的知识理解,有用!
897. 最长公共子序列
重点: 线性 dp
、LCS 问题及优化
思路:
f[i][j]
所有在第一个序列的前 i
个字母,和第二个序列的前 j
个字母构成的所有公共子序列的长度最大值a[i]
和 b[j]
是否包含在子序列当中,以此来划分所有状态集合,选与不选各两种情况,故共分为四种情况:
a[i]
不选 b[j]
时,等价于 f[i-1][j-1]
a[i]
选 b[j]
时,乍一看是 f[i-1][j]
,但是其表示的在第一个串的前 i-1
个字母中出现并在第二个串的前 j
个字母中出现的子序列。但是 f[i-1][j]
仅能保证不选 a[i]
,却无法保证 b[j]
在第二个串的前 j
个字母中。故该状态不能拿 f[i-1][j]
来直接进行转移。但是,f[i-1][j]
一定是包含了当前这个情况的,而我们最终求的 f[i][j]
又可以将 f[i-1][j]
包含掉。所以针对求 max
这个操作,集合之间有重复情况其实是不影响的,反正最后求解的是 f[i][j]
的最大值,所以 f[i-1][j]
除了覆盖当前情况外还可能覆盖了其它三种状态的部分情况,但是这不重要,它只要不漏就行,4 个状态之间的互相覆盖是不影响的。即,只要不少就行,重不重复无所谓。a[i]
不选 b[j]
时,乍一看是 f[i][j-1]
,同理也是不能保证 a[i]
被选到,但是 f[i][j-1]
一定将该种情况包含,可能会与其它三种情况进行重复,但是取整个集合的 max
操作是不影响的。a[i]
选 b[j]
时,等价于 f[i-1][j-1] + 1
f[i][j]
全部初始化为 0 即可,取 max
操作,不相等,默认就是 1更新笔记:(2021年4月3号,15:34分)
a[i] == b[j]
来进行划分
a[i] == b[j]
显然 f[i][j] = f[i-1][j-1]+1
a[i] != b[j]
显然 f[i][j] = max(f[i-1][j], f[i][j-1])
这样的集合划分显然更加容易理解一点。更新毕(2021年4月3号,15:34分)
优化代码:
#include
using namespace std;
const int N = 1010;
int n, m;
char a[N], b[N];
int f[N][N];
int main() {
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] << '\n';
return 0;
}
其实经过仔细比对,会发现,当 a[i]==b[j]
时,状态一定由 f[i-1][j-1]+1
进行转移,就不需要通过 f[i][j] = max(f[i - 1][j], f[i][j - 1]);
这个转移了。但朴素的写法,会将这个放到前面,达到:
的转移效果。图片取自 传送门:【宫水三叶の相信科学系列】求「最值问题」只需要确保「不漏」即可。
代码:
#include
#include
using namespace std;
const int N = 1005;
int n, m;
// char和int类型差别还是很大的
// int一个空间4个字节,char采用scanf读进来
// 4个char字母同时存在一个int空间中
// 这四个char就构成一个二进制数,变成了一个int数字
// 然后后面的int空间全是0,即int中存的字母远小于char中
// 所以它还是“公共子序列”,最终还有可能是正确答案,即可能出现各式各样的错误
char a[N], b[N];
int f[N][N];
int main() {
cin >> n >> 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);
}
cout << f[n][m] << endl;
return 0;
}
f[i]
只与 f[i - 1]
相关,所以可以用滚动数组。每次只存相邻两层的状态即可。
滚动数组优化是有套路的,直接在所有 f[i][j]
的第一维后面加上 & 1
就行了。
滚动数组优化代码:
#include
using namespace std;
const int N = 1010;
int n, m;
char a[N], b[N];
int f[2][N];
int main()
{
cin >> n >> m >> a + 1 >> b + 1;
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
{
f[i & 1][j] = max(f[i - 1 & 1][j], f[i & 1][j - 1]);
if (a[i] == b[j]) f[i & 1][j] = max(f[i & 1][j], f[i - 1 & 1][j - 1] + 1);
}
cout << f[n & 1][m] << endl;
return 0;
}
2021年08月25日 再度更新。
密切相关:[线性dp] aw895最长上升子序列(知识理解+重要模板题+最长上升子序列模型+LCS转化LIS) 详看下方的知识理解,有用!
源于 lc 的每日一题 1035. 不相交的线,和 [线性dp] aw1012. 友好城市(最长上升子序列模型+思维) 差不多的题,但思想却不一样。
其中三叶姐采用了lcs
的四种状态分类的方式,详细讨论了状态的重叠,但不影响正确最值的情况。并针对 lcs
与 lis
这两个重要的相似问题做以讨论、区分,令我受益匪浅。
lcs
需要两个串,状态 f[i][j]
,其中 a[i]、b[j]
这两个点的元素并不是必须要选的,并不需要包含 a[i]、b[j]
。那么 lcs
状态转移即有四种情况。lis
只需要一个串,状态 f[i]
,其中一定是以 a[i]
结尾的上升子序列长度的最大值,a[i]
是一定要被选的。虽然,lcs
可以直接判断 a[i]==b[j]
来作为状态划分,但是这种即便状态重复但并不影响最值判断的思想,也是非常重要的。