前缀 DP:f(i,j) 表示两个前缀的 LCS 长度,字符相等时取 f(i-1,j-1)+1,否则取上侧与左侧的较大值,答案是 f(n,m)。
OJ: luogu
题目 ID: U197280
难度:普及
标签:动态规划字符串二维数组
创建: 2026-09-19 18:07
更新: 2026-09-19 18:22
形式化题目
给定两个大写字母串
满足对每个
暴力解法
思路
最直白的做法就是把
"造出所有子序列"天然是一棵选择树:choose[] 记录每层的选择,dfs(dep) 只负责决定第 dep 个字符选不选,并把选中的字符依次拼进 candidate:
dep > n说明一条完整的 01 序列生成完毕,此时candidate就是一个的子序列; - 在叶子节点做两件事:检查
candidate是否是的子序列,是的话用它的大小更新答案。
"是 pos 指向 candidate 中等待匹配的字符,遇到相同的就前进一步,扫完 Y 时 pos 走到末尾就说明匹配成功。
选择序列先生成、后检查,逻辑最短,也最容易确认它没有漏掉任何方案。
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-09-19 18:07
* update_at: 2026-09-19 18:07
*/
// brute.cpp:小数据暴力解,使用 01 序列 / 选择序列递归枚举 X 的所有子序列。
// 每一层决定 X 的第 dep 个字符选或不选,选出来的字符按原顺序拼成 candidate;
// 到了叶子节点再检查 candidate 是否是 Y 的子序列,是就更新答案。
// 只有 n <= 20 左右才跑得动,只用来对拍和理解题意。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 25;
string x, y;
int n; // 序列 X 的长度,递归层数就是它
int choose[MAXN]; // choose[i] 表示 X 的第 i 个字符选(1)还是不选(0)
string candidate; // 由 X 中所有被选的字符按原顺序拼成的子序列
int ans; // 目前找到的最长公共子序列长度
// 判断 s 是否是 y 的子序列:在 y 上顺序扫描,能依次匹配完 s 就算成功。
bool is_subsequence(const string &s) {
int pos = 0;
for (int i = 0; i < (int)y.size() && pos < (int)s.size(); i++) {
if (y[i] == s[pos]) pos++;
}
return pos == (int)s.size();
}
void dfs(int dep) {
if (dep > n) {
// 完整的选择序列已经生成,检查候选子序列并统计答案
if (is_subsequence(candidate)) {
if (ans < (int)candidate.size()) ans = candidate.size();
}
return;
}
// 这一层选择 X 的第 dep 个字符:0 不选,1 选
for (int i = 0; i <= 1; i++) {
choose[dep] = i;
if (i == 1) candidate.push_back(x[dep - 1]);
dfs(dep + 1);
if (i == 1) candidate.pop_back();
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> x >> y;
n = x.size();
dfs(1);
cout << ans << "\n";
return 0;
}复杂度
- 时间:
条选择序列,每条在叶子节点花 检查,总 。 - 空间:
candidate最长,递归栈深 , 。
瓶颈
瓶颈不在检查,而在枚举量
换个角度想:判断一个候选串够不够长,其实只和"用掉了
正解
思路
一句话本质:把"枚举子序列"换成"同步推进两个前缀"——公共子序列的长度只由两个前缀决定,与具体选了哪些字符无关,于是指数级的方案被压缩成
设
边界是
问题? 如果
要。把这两个相同的字符接到
问题? 如果
它们不可能同时成为最长公共子序列的最后一个字符(那样就必须相等),所以至少有一个要被丢掉:要么
问题? 这样算为什么不会漏掉真正的答案?
因为每一步都只做"用掉
下面用样例
| B | D | C | A | B | A | ||
|---|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | |
| A | 0 | 0 | 0 | 0 | 1 | 1 | 1 |
| B | 0 | 1 | 1 | 1 | 1 | 2 | 2 |
| C | 0 | 1 | 1 | 2 | 2 | 2 | 2 |
| B | 0 | 1 | 1 | 2 | 2 | 3 | 3 |
| D | 0 | 1 | 2 | 2 | 2 | 3 | 3 |
| A | 0 | 1 | 2 | 2 | 3 | 3 | 4 |
| B | 0 | 1 | 2 | 2 | 3 | 4 | 4 |
看两个典型格子。AB 对 Y 的 BD):B。BCBA(也可取 BDAB)。
问题? 表有
不会。题面数据只有 int,四个字节一个也就 4 MB,直接开 int dp[1005][1005] 存整张表最省心:不用推敲行号怎么翻转,出错时也能直接打印任意一个
如果数据范围再大一个数量级,才需要优化空间:转移只用到"上一行同列"“上一行左一列”"本行左一列"三种来源,可以只保留两行滚动覆盖,空间从
用 std::string 存两个串最贴合题意:cin >> a >> b 一次读完整行,a.size() 就是 a[i-1] 正好是公式里的
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-09-19 18:07
* update_at: 2026-09-19 18:07
*/
// main.cpp:最长公共子序列(LCS)长度,O(nm) 的朴素 DP,直接用 string 当作字符序列。
#include <bits/stdc++.h>
using namespace std;
string a, b; // 读入的两个序列,下标从 0 开始,长度分别是 a.size()、b.size()
int dp[1005][1005]; // dp[i][j] = LCS(a 的前 i 个字符, b 的前 j 个字符)
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> a >> b;
int n = a.size(); // 序列 X 的长度
int m = b.size(); // 序列 Y 的长度
// 第 0 行、第 0 列表示其中一个串为空,公共子序列只能是空串
for (int i = 0; i <= n; i++) {
dp[i][0] = 0;
}
for (int j = 0; j <= m; j++) {
dp[0][j] = 0;
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (a[i - 1] == b[j - 1]) {
// 两个字符相同,可以直接把它们接在 LCS(a[0..i-2], b[0..j-2]) 后面
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
// 否则 X 的第 i 个字符和 Y 的第 j 个字符不可能同时出现在 LCS 的末尾
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
// dp[n][m] 就是两个完整串的 LCS 长度
cout << dp[n][m] << "\n";
return 0;
}复杂度
- 时间:
个状态,每个 转移,总 。 - 空间:二维数组
dp[1005][1005](约 4 MB)加两个串,。
总结
- 核心模型:前缀 DP。
表示两个前缀的 LCS 长度,答案 ;关键是意识到"长度只与两个前缀有关",从而把 条选择序列压缩成 个状态。 - 两种转移:
时 (这对字符值得一起用); 时 (必有一个要丢掉)。 - 实现技巧:用
int dp[1005][1005]直接存整张表,dp[i][j]就是,不必推导行的滚动映射,出问题时能直接打印表格对照公式;数据范围更大时再把其中一维压成两行。 - 暴力与正解的关系:
枚举子序列直观但重复严重,DP 正是为消除这种重复而出现的——"每个前缀对只算一次"是这一类题的通用思路,也是所有序列型 DP 的起点。