题目
https://roj.ac.cn/roj/1265/index.html
动画
解析
得到状态转移方程如下:
证明:
设
- 第一个字符串为
,第二个字符串为 的最后一个字符为 , 的最后一个字符为 - 问题:
和 的最长公共子序列长度是多少,表示为 表示为 的所有子序列 - 集合
,表示为所有的公共子序列 - 集合
中的最长的元素,设为
上面是对问题的数学描述
- 用数字
,表示 前 个元素序列 - 同理,用数字
,表示 前 个元素序列,则 - 设
为 前 个元素和 前 个元素的所有的最长公共子序列的集合
考虑
, 不可能是
也就是说
, 不可能是
也就是说
一定在 中,那么 一定是 的结尾,也就是 ,关键 与 中的哪个元素配对呢?显然 都有可能.那么我们设配对的元素为 ,可以得到
- 同理,如果
一定参与了 ,也可以得到
综上得到状态转移方程
显然边界条件为
写出一个
#include <iostream>
#include <cstring>
using namespace std;
char a[1000];
char b[1000];
int la,lb;
int f[100][100];
int main(int argc, char const *argv[])
{
cin >> a+1;
cin >> b+1;
la = strlen(a+1);
lb = strlen(b+1);
// 枚举a的前i个元素
for(int i =1 ;i<= la;i++) {
// 枚举b的前j个元素
for(int j =1;j<=lb;j++) {
f[i][j] = max(f[i-1][j],f[i][j-1]);
//考虑ai 一定出现
for(int k = j ;k >= 1 ;k--)
{
if( a[i] == b[k])
f[i][j] = max(f[i][j],f[i-1][k-1]+1);
}
//考虑bj 一定出现
for(int k = i ;k >= 1 ;k--)
{
if( a[k] == b[j])
f[i][j] = max(f[i][j],f[k-1][j-1]+1);
}
}
}
cout << f[la][lb] << endl;
return 0;
}
优化方程
为什么我们的代码为
可以想到
同样又想到
综合所得,一个新的状态转移方程.
也就是说
又显然,当
综上所述,得到
得到
#include <iostream>
#include <cstring>
using namespace std;
char a[1000];
char b[1000];
int la,lb;
//f[i][j] 表示
// s1的前i个元素
// s2的前j个元素
// 时候的答案
int f[100][100];
int main(int argc, char const *argv[])
{
cin >> a+1;
cin >> b+1;
la = strlen(a+1);
lb = strlen(b+1);
// 枚举a的前i个元素
for(int i =1 ;i<= la;i++) {
// 枚举b的前j个元素
for(int j =1;j<=lb;j++) {
//核心代码两行!!
f[i][j] = max(f[i-1][j],f[i][j-1]);
if(a[i] == b[j] ) f[i][j] = f[i - 1][j-1]+1;
}
}
cout << f[la][lb] << endl;
return 0;
}
进一步证明
证明当
已知:
,也就是说 是前 个元素形成的最长公共子序列 , 的最后两个元素相等
分情况讨论
情况1:
容易想到,此时,
情况2:
此时:
考虑
证明完毕.
最终代码
此代码可以输出
#include <iostream>
#include <cstring>
using namespace std;
char a[1000];
char b[1000];
int la,lb;
//f[i][j] 表示
// s1的前i个元素
// s2的前j个元素
// 时候的答案
int f[100][100];
// pre[i][j] = 0 表示 由左边来
// pre[i][j] = 1 表示 由上边来
// pre[i][j] = 2 表示 由斜对角边来
int pre[100][100];
const int up = 0;
const int Left = 1;
const int ul = 2;
int idx;
char ans[100];
int main(int argc, char const *argv[])
{
cin >> a+1;
cin >> b+1;
la = strlen(a+1);
lb = strlen(b+1);
// 枚举a的前i个元素
for(int i =1 ;i<= la;i++) {
// 枚举b的前j个元素
for(int j =1;j<=lb;j++) {
int t1 = f[i-1][j]; //上边
int t2 = f[i][j-1]; //左边
if( t1 > t2) {
f[i][j] = t1;
pre[i][j] = up;
}
else {
f[i][j] = t2;
pre[i][j] = Left;
}
if(a[i] == b[j] )
{
f[i][j] = f[i - 1][j-1]+1;
pre[i][j] = ul;
}
}
}
//输出答案
cout << f[la][lb] << endl;
// 输出lcs 对应的字符串
int i = la;
int j = lb;
while( i != 0 && j != 0) {
if( pre[i][j] == ul)
{
ans[++idx] = a[i];
i--;
j--;
}
else if( pre[i][j] == up) {
i--;
}
else j--;
}
for(int i = f[la][lb]; i>=1 ;--i ) // i: 1->n
{
cout << ans[i];
}
std::cout << "\n";
return 0;
}
核心思想:
- 每一个问题都对应一个集合
- 分类讨论,分解集合
- 子集包含,问题转化
题目练习
基础模板与还原
- [
luogu U197280: 【模板】最长公共子序列] (洛谷 U197280) 标准双串二维 DP - [
noi_openjudge ch0206-1808: 公共子序列] - [
leetcodecn longest-common-subsequence: 最长公共子序列] (LeetCode 1143) - [
luogu P2758: 编辑距离] (洛谷 P2758) 字符增删改代价转移,双串匹配经典
模型转化与方案计数
- [
leetcodecn minimum-insertion-steps-to-make-a-string-palindrome: 让字符串成为回文串的最少插入次数] (LeetCode 1312) 原串与反转串的 LCS 转化 - [
luogu P2516: [HAOI2010] 最长公共子序列] (洛谷 P2516) LCS 长度与方案数计数(容斥去重)
跨主题进阶优化
- [
luogu P1439: 【模板】最长公共子序列] (洛谷 P1439) 全排列 LCS 映射转 LIS,优化至
补充
这里其实用到了一个集合的知识: 集合并集与最大值
对于任意两个非空集合 A 和 B,它们的并集
下列等式恒成立:
我们可以通过一个简单的例子来理解它。
假设:
首先,我们来计算等式右边的值:
接着,我们计算等式左边的值:
可以看到,等式两边都等于 10,所以
这个结论的直观解释是:两个集合合并后,它们的最大值必然是原来两个集合各自最大值中较大的那一个。因为并集包含所有来自 A 和 B 的元素,所以新的最大值只可能来自 A 的最大值或 B 的最大值,不可能凭空出现一个更大的数。