把展开过程倒过来看:区间能折成一个字母当且仅当存在规则 c→xy 与分裂点把区间切两半;按区间长度递推的区间 DP,复杂度 O(L^3)。
OJ: luogu
题目 ID: P4290
难度:普及+/提高-
标签:动态规划区间 DP字符串
创建: 2026-10-23 23:09
更新: 2026-10-23 23:19
形式化题目
字母表固定为
名字的生成方式是:先取单个字母作为初始名,然后反复执行「把当前串中的某一个字母按某条规则换成两个字母」。每执行一次替换,串长加
给定长度为
暴力解法
思路
生成的每一步都只在串内做一次「一变二」,反过来看就是把相邻的两个字符折回成一个字符。于是问题的核心变成:
的某个子串能否折叠成字母 ?
折叠一棵树如果根的替换是
于是可以直接写递归:
且 (长度 1,不需要任何替换);或 - 存在规则
与分裂点 ,使 折成 、 折成 。
答案就是所有能让整体
用样例验证一下:规则为 IIII。
I折成(长度 1)。 II折成(规则 )。 III用分裂点:左 I折成、右 II折成,需要规则 ,没有;用 :左 II折成、右 I折成,需要 ,也没有。所以 III折不成任何字母。IIII用:左、右都折成 ,需要规则 ,命中 与 。于是 IIII折成或 。
按 IN,与样例一致。
代码
/**
* 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-10-23 23:12
* update_at: 2026-10-23 23:19
*/
// brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
// 直接照“折叠的最后一步”递归:S[l..r] 折成 c,当且仅当存在规则 c -> xy 与分裂点 m,
// 使左半折成 x、右半折成 y。这里不做记忆化,重复子区间会被反复重算,
// 复杂度指数级,只适合 L <= 20 的小数据(正好对应 30% 部分分)。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXL = 205; // L <= 200
const int MAXC = 4; // 字母表只有 W I N G 四个
ll rule_cnt[MAXC]; // 每个字母的规则条数
vector<pair<int,int> > rule[MAXC]; // rule[c]:字母 c 的所有右部 (x, y),元素是字母编号
char s[MAXL]; // 1-indexed 的目标名字
ll n; // 名字长度 L
// 把字母映射到 0..3 的编号,顺序 W I N G
int to_id(char ch) {
if (ch == 'W') return 0;
if (ch == 'I') return 1;
if (ch == 'N') return 2;
return 3; // 'G'
}
char id_to_char[4] = {'W', 'I', 'N', 'G'};
// fold(c, l, r):S[l..r] 能否折叠成单个字母 c
bool fold(int c, int l, int r) {
if (l == r) {
return s[l] == id_to_char[c];
}
for (auto &p : rule[c]) { // 枚举最后一步替换 c -> x y
int x = p.first;
int y = p.second;
for (int m = l; m < r; m++) { // 枚举分裂点
if (fold(x, l, m) && fold(y, m + 1, r)) {
return true;
}
}
}
return false;
}
void read_data() {
for (int c = 0; c < MAXC; c++) {
cin >> rule_cnt[c];
}
for (int c = 0; c < MAXC; c++) {
for (int k = 0; k < rule_cnt[c]; k++) {
string t;
cin >> t;
rule[c].push_back(make_pair(to_id(t[0]), to_id(t[1])));
}
}
string name;
cin >> name;
n = name.size();
for (int i = 1; i <= n; i++) {
s[i] = name[i - 1];
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
read_data();
string ans;
for (int c = 0; c < MAXC; c++) {
if (fold(c, 1, n)) ans.push_back(id_to_char[c]);
}
if (ans.empty()) cout << "The name is wrong!" << "\n";
else cout << ans << "\n";
return 0;
}复杂度
递归不做任何记忆化,同一对
瓶颈
fold(x, l, m) 的结果只与
真正的浪费点只有一个:子区间的折叠结果没有被留下来复用。
正解
关键观察
把暴力里「
既然依赖关系总是「短区间
思路
- 长度 1:
。长度 1 的串不需要任何替换,它本身就是那个字母。 - 长度
:对每个左端点 (右端点 ),枚举分裂点 ,再枚举规则 : 若 且 ,就把 加入 。 - 答案:
中的字母按 顺序输出;为空则输出 The name is wrong!。
以样例的 IIII 为例,按填表顺序把各区间能折成的字母集合列在下表(行是区间长度,列是该长度下的所有区间):
| 区间长度 | 长度内的所有区间及其集合 |
|---|---|
| 1 | |
| 2 | |
| 3 | |
| 4 |
长度 2 的每个区间都是 II,命中规则
存储上用 can[l][r][c](bool)最直白:三个下标分别是左端点、右端点、字母编号(rule[c] 存
为什么按长度递增就够
递推
代码
/**
* 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-10-23 23:12
* update_at: 2026-10-23 23:19
*/
// 正解:区间 DP。can[l][r][c] 表示子串 S[l..r] 能否折叠成字母 c。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXL = 205; // L <= 200
const int MAXC = 4; // 字母表只有 W I N G 四个
ll len_limit[MAXC]; // 每个字母的规则条数
vector<pair<int,int> > rule[MAXC]; // rule[c]:字母 c 的所有右部 (x, y),元素是字母编号
bool can[MAXL][MAXL][MAXC]; // can[l][r][c]:S[l..r] 能否折叠成字母 c
char s[MAXL]; // 1-indexed 的目标名字
ll n; // 名字长度 L
// 把字母映射到 0..3 的编号,顺序与题目输出顺序一致:W I N G
int to_id(char ch) {
if (ch == 'W') return 0;
if (ch == 'I') return 1;
if (ch == 'N') return 2;
return 3; // 'G'
}
char id_to_char[4] = {'W', 'I', 'N', 'G'};
void read_data() {
for (int c = 0; c < MAXC; c++) {
cin >> len_limit[c];
}
for (int c = 0; c < MAXC; c++) {
for (int k = 0; k < len_limit[c]; k++) {
string t;
cin >> t;
rule[c].push_back(make_pair(to_id(t[0]), to_id(t[1])));
}
}
string name;
cin >> name;
n = name.size();
for (int i = 1; i <= n; i++) {
s[i] = name[i - 1];
}
}
void solve() {
// 长度 1:不需要替换,只能是被折叠的那个字母本身
for (int i = 1; i <= n; i++) {
can[i][i][to_id(s[i])] = true;
}
// 按区间长度从小到大递推,保证用到的更短区间已经算好
for (int len = 2; len <= n; len++) {
for (int l = 1; l + len - 1 <= n; l++) {
int r = l + len - 1;
for (int m = l; m < r; m++) { // 枚举分裂点:左半 S[l..m],右半 S[m+1..r]
for (int c = 0; c < MAXC; c++) {
if (can[l][r][c]) continue; // 已经能折成 c,跳过
for (auto &p : rule[c]) {
if (can[l][m][p.first] && can[m + 1][r][p.second]) {
can[l][r][c] = true;
break;
}
}
}
}
}
}
}
void print_answer() {
string ans;
for (int c = 0; c < MAXC; c++) {
if (can[1][n][c]) ans.push_back(id_to_char[c]);
}
if (ans.empty()) cout << "The name is wrong!" << "\n";
else cout << ans << "\n";
}
signed main() {
ios::sync_with_stdio(false); cin.tie(0);
read_data();
solve();
print_answer();
return 0;
}复杂度
状态数
break)。空间
总结
这道题的关键不在「怎么搜」,而在把展开过程倒过来看:一步替换就是一变二,倒过来就是把相邻两段合并,于是每个区间的答案只由它的两半决定——这是典型的区间 DP 结构。
- 暴力递归把「区间能否折成
」直接当递归算,问题在于同一区间被重复求值。 - 正解把这个判断做成状态
,按区间长度递增填表,每个状态只算一次,指数级降到 。
写代码时最容易漏的是 长度 1 的初始化: