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

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:

  1. 令 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;
  2. 令 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 恰好进行一次替换的方案数。核心是如何高效匹配规则

核心观察
  1. 替换的数学描述

    设替换发生在位置 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 完全相同
  2. 差异区间

    由于替换前后只在替换区间内不同,设 [ 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

  3. 关键转化

    将规则和文本都转换为字符对 : ( 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) 的算法。

步骤概述
  1. 规则预处理

    • 将每个规则 ( s 1 , s 2 ) (s_1,s_2) (s1,s2) 转换为字符对序列 P i P_i Pi,插入 AC 自动机。
    • 记录每个规则的长度结束节点
  2. 询问处理

    • 对每个询问 ( 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。
  3. 离线查询处理

    • 将所有规则长度和查询的 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;
}

功能分析

算法流程
  1. 预处理阶段

    • 将每个规则转换为字符对序列并插入 AC 自动机
    • 记录每个规则的长度和结束节点
    • 构建 fail 树(用于快速查询祖先节点)
  2. 询问处理阶段

    • 对每个询问:
      • 计算差异区间 [ 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] 所需的最小规则长度
  3. 离线统计阶段

    • 将所有长度值(规则长度和查询 L 0 L_0 L0)离散化
    • 在 fail 树上 DFS,用树状数组维护当前路径上的规则长度
    • 对每个查询,统计树状数组中长度 ≥ L 0 \geq L_0 ≥L0 的规则数
关键优化
  1. 字符对编码

    将 ( a , b ) (a,b) (a,b) 映射为 [ 0 , 675 ] [0,675] [0,675] 的整数,实现紧凑表示。

  2. 记忆化转移

    AC 自动机匹配时记忆化转移结果,避免重复跳 fail,均摊 O ( 1 ) O(1) O(1)。

  3. 离线树状数组

    利用 fail 树的 DFS 序,用树状数组动态维护祖先节点的规则长度,实现 O ( log ⁡ n ) O(\log n) O(logn) 查询。

  4. 离散化

    将所有长度值离散化,减少树状数组大小。


各种学习资料,助力大家一站式学习和提升!!!

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;
}
相关推荐
zhuqiyua4 小时前
第一次课程家庭作业
c++
只是懒得想了4 小时前
C++实现密码破解工具:从MD5暴力破解到现代哈希安全实践
c++·算法·安全·哈希算法
m0_736919105 小时前
模板编译期图算法
开发语言·c++·算法
玖釉-5 小时前
深入浅出:渲染管线中的抗锯齿技术全景解析
c++·windows·图形渲染
【心态好不摆烂】5 小时前
C++入门基础:从 “这是啥?” 到 “好像有点懂了”
开发语言·c++
dyyx1115 小时前
基于C++的操作系统开发
开发语言·c++·算法
AutumnorLiuu5 小时前
C++并发编程学习(一)——线程基础
开发语言·c++·学习
m0_736919105 小时前
C++安全编程指南
开发语言·c++·算法
阿猿收手吧!5 小时前
C++ std::lock与std::scoped_lock深度解析:从死锁解决到安全实践
开发语言·c++
2301_790300965 小时前
C++符号混淆技术
开发语言·c++·算法