2025信奥赛C++提高组csp-s复赛真题及题解:谐音替换

题目描述
小 W 是一名喜欢语言学的算法竞赛选手。在语言学中,谐音替换是指将原有的字词替换为读音相同或相近的字词。小 W 发现,谐音替换的过程可以用字符串来进行描述。具体地,小 W 将谐音替换定义为以下字符串问题:
给定 n n n 个字符串二元组,第 i i i ( 1 ≤ i ≤ n 1 \leq i \leq n 1≤i≤n) 个字符串二元组为 ( s i , 1 , s i , 2 ) (s_{i,1}, s_{i,2}) (si,1,si,2),满足 ∣ s i , 1 ∣ = ∣ s i , 2 ∣ |s_{i,1}| = |s_{i,2}| ∣si,1∣=∣si,2∣,其中 ∣ s ∣ |s| ∣s∣ 表示字符串 s s s 的长度。
对于字符串 s s s,定义 s s s 的替换如下:
- 对于 s s s 的某个子串 y y y,若存在 1 ≤ i ≤ n 1 \leq i \leq n 1≤i≤n 满足 y = s i , 1 y = s_{i,1} y=si,1,则将 y y y 替换为 y ′ = s i , 2 y' = s_{i,2} y′=si,2。具体地,设 s = x + y + z s = x + y + z s=x+y+z,其中 x x x 和 z z z 可以为空,"+" 表示字符串拼接,则 s s s 的替换将得到字符串 s ′ = x + y ′ + z s' = x + y' + z s′=x+y′+z。
小 W 提出了 q q q 个问题,第 j j j ( 1 ≤ j ≤ q 1 \leq j \leq q 1≤j≤q) 个问题会给定两个不同 的字符串 t j , 1 , t j , 2 t_{j,1}, t_{j,2} tj,1,tj,2,她想知道有多少种字符串 t j , 1 t_{j,1} tj,1 的替换能够得到字符串 t j , 2 t_{j,2} tj,2。两种 s s s 的替换不同当且仅当子串 y y y 的位置不同或用于替换的二元组 ( s i , 1 , s i , 2 ) (s_{i,1}, s_{i,2}) (si,1,si,2) 不同 ,即 x , z x, z x,z 不同或 i i i 不同。你需要回答小 W 提出的所有问题。
输入格式
输入的第一行包含两个正整数 n , q n, q n,q,分别表示字符串二元组的数量和小 W 提出的问题的数量。
输入的第 i + 1 i+1 i+1 ( 1 ≤ i ≤ n 1 \leq i \leq n 1≤i≤n) 行包含两个字符串 s i , 1 , s i , 2 s_{i,1}, s_{i,2} si,1,si,2,表示第 i i i 个字符串二元组。
输入的第 j + n + 1 j+n+1 j+n+1 ( 1 ≤ j ≤ q 1 \leq j \leq q 1≤j≤q) 行包含两个字符串 t j , 1 , t j , 2 t_{j,1}, t_{j,2} tj,1,tj,2,表示小 W 提出的第 j j j 个问题。
输出格式
输出 q q q 行,其中第 j j j ( 1 ≤ j ≤ q 1 \leq j \leq q 1≤j≤q) 行包含一个非负整数,表示替换后得到字符串 t j , 2 t_{j,2} tj,2 的字符串 t j , 1 t_{j,1} tj,1 的替换的数量。
输入输出样例 1
输入 1
4 2
xabcx xadex
ab cd
bc de
aa bb
xabcx xadex
aaaa bbbb
输出 1
2
0
输入输出样例 2
输入 2
3 4
a b
b c
c d
aa bb
aa b
a c
b a
输出 2
0
0
0
0
说明/提示
【样例 1 解释】
对于小 W 的第一个询问,共有 2 2 2 种 t 1 , 1 t_{1,1} t1,1 的替换能够得到 t 1 , 2 t_{1,2} t1,2:
- 令 x , z x, z x,z 均为空串, y = xabcx y = \texttt{xabcx} y=xabcx, i = 1 i = 1 i=1,则 y ′ = xadex y' = \texttt{xadex} y′=xadex,替换后得到 xadex \texttt{xadex} xadex;
- 令 x = xa x = \texttt{xa} x=xa, y = bc y = \texttt{bc} y=bc, z = x z = \texttt{x} z=x, i = 3 i = 3 i=3,则 y ′ = de y' = \texttt{de} y′=de,替换后得到 xadex \texttt{xadex} xadex。
【数据范围】
设 L 1 = ∑ i = 1 n ∣ s i , 1 ∣ + ∣ s i , 2 ∣ L_1 = \sum_{i=1}^{n} |s_{i,1}| + |s_{i,2}| L1=∑i=1n∣si,1∣+∣si,2∣, L 2 = ∑ j = 1 q ∣ t j , 1 ∣ + ∣ t j , 2 ∣ L_2 = \sum_{j=1}^{q} |t_{j,1}| + |t_{j,2}| L2=∑j=1q∣tj,1∣+∣tj,2∣。对于所有测试数据,保证:
- 1 ≤ n , q ≤ 2 × 10 5 1 \leq n, q \leq 2 \times 10^5 1≤n,q≤2×105;
- 2 ≤ L 1 , L 2 ≤ 5 × 10 6 2 \leq L_1, L_2 \leq 5 \times 10^6 2≤L1,L2≤5×106;
- 对于所有 1 ≤ i ≤ n 1 \leq i \leq n 1≤i≤n, s i , 1 , s i , 2 s_{i,1}, s_{i,2} si,1,si,2 均仅包含小写英文字母,且 ∣ s i , 1 ∣ = ∣ s i , 2 ∣ |s_{i,1}| = |s_{i,2}| ∣si,1∣=∣si,2∣;
- 对于所有 1 ≤ j ≤ q 1 \leq j \leq q 1≤j≤q, t j , 1 , t j , 2 t_{j,1}, t_{j,2} tj,1,tj,2 均仅包含小写英文字母,且 t j , 1 ≠ t j , 2 t_{j,1} \neq t_{j,2} tj,1=tj,2。
| 测试点编号 | n , q ≤ n, q \leq n,q≤ | L 1 , L 2 ≤ L_1, L_2 \leq L1,L2≤ | 特殊性质 |
|---|---|---|---|
| 1 , 2 1, 2 1,2 | 10 2 10^2 102 | 200 200 200 | 无 |
| 3 ∼ 5 3 \sim 5 3∼5 | 10 3 10^3 103 | 2 000 2\,000 2000 | ^ |
| 6 6 6 | ^ | 10 6 10^6 106 | AB |
| 7 , 8 7, 8 7,8 | 10 4 10^4 104 | ^ | A |
| 9 , 10 9, 10 9,10 | 2 × 10 5 2 \times 10^5 2×105 | ^ | B |
| 11 , 12 11, 12 11,12 | ^ | 2 × 10 6 2 \times 10^6 2×106 | 无 |
| 13 , 14 13, 14 13,14 | ^ | 5 × 10 6 5 \times 10^6 5×106 | A |
| 15 , 16 15, 16 15,16 | ^ | ^ | B |
| 17 ∼ 20 17 \sim 20 17∼20 | ^ | ^ | 无 |
特殊性质 A: q = 1 q = 1 q=1。
特殊性质 B:定义字符串 s s s 为特别的 ,当且仅当字符串 s s s 仅包含字符 a a a 和 b b b,且字符 b b b 在 s s s 中出现恰好 一次。对于所有 1 ≤ i ≤ n 1 \leq i \leq n 1≤i≤n, s i , 1 , s i , 2 s_{i,1}, s_{i,2} si,1,si,2 均为特别的,且对于所有 1 ≤ j ≤ q 1 \leq j \leq q 1≤j≤q, t j , 1 , t j , 2 t_{j,1}, t_{j,2} tj,1,tj,2 均为特别的。
思路分析
本题需要统计从 t 1 t_1 t1 到 t 2 t_2 t2 恰好进行一次替换的方案数。核心是如何高效匹配规则。
核心观察
-
替换的数学描述
设替换发生在位置 l l l(0-based),使用规则 i i i(长度为 m m m)。则必须满足:
- t 1 [ l : l + m − 1 ] = s i , 1 t_1[l:l+m-1] = s_{i,1} t1[l:l+m−1]=si,1
- t 2 [ l : l + m − 1 ] = s i , 2 t_2[l:l+m-1] = s_{i,2} t2[l:l+m−1]=si,2
- 在其他位置 t 1 t_1 t1 和 t 2 t_2 t2 完全相同
-
差异区间
由于替换前后只在替换区间内不同,设 [ L , R ] [L,R] [L,R] 为 t 1 t_1 t1 和 t 2 t_2 t2 不同的连续区间。
替换区间必须完全覆盖 [ L , R ] [L,R] [L,R],即:
l ≤ L ≤ R ≤ l + m − 1 l \leq L \leq R \leq l+m-1 l≤L≤R≤l+m−1 -
关键转化
将规则和文本都转换为字符对 : ( t 1 [ j ] , t 2 [ j ] ) (t_1[j], t_2[j]) (t1[j],t2[j]) 和 ( s i , 1 [ k ] , s i , 2 [ k ] ) (s_{i,1}[k], s_{i,2}[k]) (si,1[k],si,2[k])。
这样规则 i i i 对应一个字符对序列,匹配条件是整个字符对序列完全相等。
高效算法设计
由于 n , q n,q n,q 和总长度都很大,需要 O ( 总长度 ⋅ log ) O(\text{总长度} \cdot \log) O(总长度⋅log) 的算法。
步骤概述
-
规则预处理
- 将每个规则 ( s 1 , s 2 ) (s_1,s_2) (s1,s2) 转换为字符对序列 P i P_i Pi,插入 AC 自动机。
- 记录每个规则的长度 和结束节点。
-
询问处理
- 对每个询问 ( t 1 , t 2 ) (t_1,t_2) (t1,t2):
- 检查长度,计算差异区间 [ L , R ] [L,R] [L,R]。
- 将 t 1 , t 2 t_1,t_2 t1,t2 转为字符对序列 T T T。
- 在 AC 自动机上运行 T T T,对每个位置 j j j 得到状态节点 u j u_j uj。
- 对 j ≥ R j \geq R j≥R,计算最小长度 L 0 = j − L + 1 L_0 = j-L+1 L0=j−L+1,将查询 ( L 0 , qid ) (L_0,\text{qid}) (L0,qid) 挂在节点 u j u_j uj。
- 对每个询问 ( t 1 , t 2 ) (t_1,t_2) (t1,t2):
-
离线查询处理
- 将所有规则长度和查询的 L 0 L_0 L0 离散化。
- 在 AC 自动机的 fail 树上进行 DFS,用树状数组维护当前路径上的规则长度。
- 处理每个节点上的查询:统计树状数组中长度 ≥ L 0 \geq L_0 ≥L0 的规则数。
时间复杂度
- 构建 AC 自动机: O ( L 1 ) O(L_1) O(L1)
- 运行所有询问: O ( L 2 ) O(L_2) O(L2)
- 离线处理: O ( ( n + Q ) log M ) O((n+Q)\log M) O((n+Q)logM),其中 Q Q Q 是总查询数(不超过 L 2 L_2 L2), M M M 是离散化值域大小。
空间复杂度
O ( L 1 + L 2 + n + Q ) O(L_1 + L_2 + n + Q) O(L1+L2+n+Q),可控制在几百 MB 内。
代码实现
cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
// 字符对编码 (a,b) -> 0~675
inline int enc(char a, char b) {
return (a - 'a') * 26 + (b - 'a');
}
// AC自动机节点
struct N {
unordered_map<int, int> nxt; // 转移
int fail; // fail指针
vector<int> rid; // 以此节点结尾的规则id
N() : fail(0) {}
};
vector<N> ac; // AC自动机节点池
int rt; // 根节点
vector<int> rl; // 规则长度
vector<int> rn; // 规则结束节点
// 插入一个模式串(字符对序列)
int ins(const vector<int>& s) {
int u = rt;
for (int c : s) {
if (!ac[u].nxt.count(c)) {
ac[u].nxt[c] = ac.size();
ac.emplace_back();
}
u = ac[u].nxt[c];
}
return u;
}
// 构建AC自动机和fail树
void bd(vector<vector<int>>& tr) {
queue<int> q;
for (auto& p : ac[rt].nxt) {
int v = p.second;
ac[v].fail = rt;
q.push(v);
}
while (!q.empty()) {
int u = q.front(); q.pop();
for (auto& p : ac[u].nxt) {
int c = p.first, v = p.second;
int f = ac[u].fail;
while (f != rt && !ac[f].nxt.count(c)) f = ac[f].fail;
if (ac[f].nxt.count(c)) f = ac[f].nxt[c];
ac[v].fail = f;
q.push(v);
}
}
// 构建fail树(反向边)
tr.resize(ac.size());
for (int i = 1; i < ac.size(); ++i)
tr[ac[i].fail].push_back(i);
}
// 记忆化转移(避免重复跳fail)
int gt(int u, int c) {
if (ac[u].nxt.count(c)) return ac[u].nxt[c];
if (u == rt) return rt;
int r = gt(ac[u].fail, c);
ac[u].nxt[c] = r; // 记忆化
return r;
}
// 树状数组
struct BIT {
vector<int> t;
int n;
BIT(int n) : n(n), t(n + 1, 0) {}
void add(int x, int v) {
while (x <= n) t[x] += v, x += x & -x;
}
int sum(int x) {
int r = 0;
while (x) r += t[x], x -= x & -x;
return r;
}
int qry(int l, int r) {
if (l > r) return 0;
return sum(r) - sum(l - 1);
}
};
// 查询结构体
struct Qry {
int L0, id;
};
vector<ll> ans; // 每个询问的答案
vector<vector<Qry>> nq; // 每个节点上的查询
vector<int> vl; // 离散化数组(存储所有长度值)
// DFS fail树处理查询
void dfs(int u, BIT& bt, const vector<vector<int>>& tr) {
// 插入当前节点的规则
for (int rid : ac[u].rid) {
int len = rl[rid];
int idx = lower_bound(vl.begin(), vl.end(), len) - vl.begin() + 1;
bt.add(idx, 1);
}
// 处理当前节点的查询
for (auto& q : nq[u]) {
int L0 = q.L0;
int idx = lower_bound(vl.begin(), vl.end(), L0) - vl.begin() + 1;
ans[q.id] += bt.qry(idx, bt.n);
}
// 递归子节点
for (int v : tr[u]) dfs(v, bt, tr);
// 回溯删除规则
for (int rid : ac[u].rid) {
int len = rl[rid];
int idx = lower_bound(vl.begin(), vl.end(), len) - vl.begin() + 1;
bt.add(idx, -1);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int n, q;
cin >> n >> q;
// 初始化
ac.emplace_back();
rt = 0;
rl.reserve(n);
rn.reserve(n);
vl = vector<int>(); // 用于离散化
// 读入规则
for (int i = 0; i < n; ++i) {
string s1, s2;
cin >> s1 >> s2;
int m = s1.size();
rl.push_back(m);
vector<int> p(m);
for (int j = 0; j < m; ++j)
p[j] = enc(s1[j], s2[j]);
int node = ins(p);
rn.push_back(node);
ac[node].rid.push_back(i);
vl.push_back(m); // 收集规则长度
}
// 构建AC自动机和fail树
vector<vector<int>> ft;
bd(ft);
// 准备处理询问
ans.assign(q, 0);
nq.resize(ac.size());
// 处理每个询问
for (int qid = 0; qid < q; ++qid) {
string a, b;
cin >> a >> b;
int m = a.size();
if (m != b.size()) continue; // 长度不同无解
// 计算差异区间 [L,R]
int L = -1, R = -1;
for (int i = 0; i < m; ++i) {
if (a[i] != b[i]) {
if (L == -1) L = i;
R = i;
}
}
// 检查区间外是否相同
bool ok = true;
for (int i = 0; i < m; ++i) {
if (i < L || i > R) {
if (a[i] != b[i]) {
ok = false;
break;
}
}
}
if (!ok || L == -1) continue; // 区间不连续或完全相同
// 转换为字符对序列
vector<int> txt(m);
for (int i = 0; i < m; ++i)
txt[i] = enc(a[i], b[i]);
// 在AC自动机上运行
int u = rt;
for (int j = 0; j < m; ++j) {
u = gt(u, txt[j]);
if (j >= R) {
int L0 = j - L + 1; // 所需最小长度
nq[u].push_back({L0, qid});
vl.push_back(L0); // 收集查询长度
}
}
}
// 离散化长度值
sort(vl.begin(), vl.end());
vl.erase(unique(vl.begin(), vl.end()), vl.end());
int sz = vl.size();
// 树状数组
BIT bt(sz);
// DFS fail树处理所有查询
dfs(rt, bt, ft);
// 输出答案
for (int i = 0; i < q; ++i)
cout << ans[i] << "\n";
return 0;
}
功能分析
算法流程
-
预处理阶段
- 将每个规则转换为字符对序列并插入 AC 自动机
- 记录每个规则的长度和结束节点
- 构建 fail 树(用于快速查询祖先节点)
-
询问处理阶段
- 对每个询问:
- 计算差异区间 [ L , R ] [L,R] [L,R]
- 将文本转为字符对序列并运行 AC 自动机
- 对每个满足 j ≥ R j \geq R j≥R 的位置,生成查询 ( L 0 , qid ) (L_0,\text{qid}) (L0,qid) 挂在当前节点
- 其中 L 0 = j − L + 1 L_0 = j-L+1 L0=j−L+1 表示覆盖 [ L , j ] [L,j] [L,j] 所需的最小规则长度
- 对每个询问:
-
离线统计阶段
- 将所有长度值(规则长度和查询 L 0 L_0 L0)离散化
- 在 fail 树上 DFS,用树状数组维护当前路径上的规则长度
- 对每个查询,统计树状数组中长度 ≥ L 0 \geq L_0 ≥L0 的规则数
关键优化
-
字符对编码
将 ( a , b ) (a,b) (a,b) 映射为 [ 0 , 675 ] [0,675] [0,675] 的整数,实现紧凑表示。
-
记忆化转移
AC 自动机匹配时记忆化转移结果,避免重复跳 fail,均摊 O ( 1 ) O(1) O(1)。
-
离线树状数组
利用 fail 树的 DFS 序,用树状数组动态维护祖先节点的规则长度,实现 O ( log n ) O(\log n) O(logn) 查询。
-
离散化
将所有长度值离散化,减少树状数组大小。
各种学习资料,助力大家一站式学习和提升!!!
cpp
#include<bits/stdc++.h>
using namespace std;
int main(){
cout<<"########## 一站式掌握信奥赛知识! ##########";
cout<<"############# 冲刺信奥赛拿奖! #############";
cout<<"###### 课程购买后永久学习,不受限制! ######";
return 0;
}
1、csp信奥赛高频考点知识详解及案例实践:
CSP信奥赛C++动态规划:
https://blog.csdn.net/weixin_66461496/category_13096895.html点击跳转
CSP信奥赛C++标准模板库STL:
https://blog.csdn.net/weixin_66461496/category_13108077.html 点击跳转
信奥赛C++提高组csp-s知识详解及案例实践:
https://blog.csdn.net/weixin_66461496/category_13113932.html
2、csp信奥赛冲刺一等奖有效刷题题解:
CSP信奥赛C++初赛及复赛高频考点真题解析(持续更新):https://blog.csdn.net/weixin_66461496/category_12808781.html 点击跳转
CSP信奥赛C++一等奖通关刷题题单及题解(持续更新):https://blog.csdn.net/weixin_66461496/category_12673810.html 点击跳转
3、GESP C++考级真题题解:

GESP(C++ 一级+二级+三级)真题题解(持续更新):https://blog.csdn.net/weixin_66461496/category_12858102.html 点击跳转

GESP(C++ 四级+五级+六级)真题题解(持续更新):https://blog.csdn.net/weixin_66461496/category_12869848.html 点击跳转

GESP(C++ 七级+八级)真题题解(持续更新):
https://blog.csdn.net/weixin_66461496/category_13117178.html
4、CSP信奥赛C++竞赛拿奖视频课:
https://edu.csdn.net/course/detail/40437 点击跳转

· 文末祝福 ·
cpp
#include<bits/stdc++.h>
using namespace std;
int main(){
cout<<"跟着王老师一起学习信奥赛C++";
cout<<" 成就更好的自己! ";
cout<<" csp信奥赛一等奖属于你! ";
return 0;
}