字符串变换

把字符替换函数视为字符集合上的置换,用二进制倍增预处理字符经过 $2^j$ 次替换后的结果。

OJ: shumeng

题目 ID: CSP202409B

难度:普及-

标签:字符串置换倍增

日期: 2026-07-31 16:21

形式化题目

给定一个字符替换函数:每个字符替换为另一个字符(未定义的字符保持不变),以及一个初始字符串 ss。有 mm 次查询,每次给定替换次数 kk,输出 ss 经过函数重复作用 kk 次后的结果。

注意字符串与字符对都用 # 包裹,字符串内部可能含空格,需要按行读取。

思路

kk 最大可达 10910^9,逐次替换显然不可行,需要把“重复作用 kk 次”压缩成对数次跳转。

朴素做法:直接重复替换

先看直接模拟:每轮把整串字符同时替换一次,重复 kk 轮。

cpp
/**
 * 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-07-31 16:21
 * update_at: 2026-08-17 22:39
 */
// brute.cpp:小数据暴力解,直接重复执行 k 次字符替换,只适合 k 很小的数据。
#include <bits/stdc++.h>
using namespace std;

const int MAXC = 128;

int next_char[MAXC]; // 每个字符经过一次替换后的字符

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    // 输入行形如 #Hello World#,需要整行读取
    string line;
    getline(cin, line);
    string initial = line.substr(1, line.size() - 2); // 去掉两侧的 # 得到初始字符串

    int n;
    cin >> n;
    getline(cin, line);
    // 未定义的字符替换为自身
    for (int i = 0; i < MAXC; i++) next_char[i] = i;
    for (int i = 0; i < n; i++) {
        getline(cin, line); // 形如 #a b# 或 #x y#
        next_char[(unsigned char)line[1]] = (unsigned char)line[2];
    }

    int m;
    cin >> m;
    for (int query = 0; query < m; query++) {
        int k;
        cin >> k;
        // 每轮把整串字符同时替换一次
        string result = initial;
        for (int step = 0; step < k; step++) {
            for (int i = 0; i < (int)result.size(); i++) {
                result[i] = (char)next_char[(unsigned char)result[i]];
            }
        }
        cout << '#' << result << "#\n";
    }

    return 0;
}

复杂度与 kk 成正比,只适合小数据验证规则。

把替换看成字符函数

建立 next_char[ch]:未定义的字符指向自身,输入的字符对覆盖对应映射。函数作用在有限的字符集合上,是一个置换,字符永远只会在这有限集合中移动。

二进制倍增

定义 jump[j][ch] 表示字符 ch 经过 2j2^j 次替换后得到的字符:

jump[0][ch]=f(ch),jump[j][ch]=jump[j1][jump[j1][ch]]jump[0][ch] = f(ch), \qquad jump[j][ch] = jump[j-1][\,jump[j-1][ch]\,]。

预处理 3131 层即可覆盖 k109<230k \le 10^9 < 2^{30}。查询时把 kk 写成二进制,从低位到高位处理:第 jj 位为 1 就把当前字符替换为 jump[j][current]。初始字符串的每个字符独立完成这个过程,最后补回两侧的 # 输出。

代码

cpp
/**
 * 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-07-31 16:21
 * update_at: 2026-08-17 22:39
 */
#include <bits/stdc++.h>
using namespace std;

const int MAXC = 128; // ASCII 字符范围
const int LOG = 31;   // 2^30 > 1e9,31 层足够覆盖最大变换次数

int next_char[MAXC];  // next_char[c] 表示字符 c 经过一次替换后的字符
int jump[LOG][MAXC];  // jump[j][c] 表示字符 c 经过 2^j 次替换后的字符

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    // 输入行形如 #Hello World#,需要整行读取
    string line;
    getline(cin, line);
    string initial = line.substr(1, line.size() - 2); // 去掉两侧的 # 得到初始字符串

    int n;
    cin >> n;
    getline(cin, line);
    // 未定义的字符替换为自身
    for (int i = 0; i < MAXC; i++) next_char[i] = i;
    for (int i = 0; i < n; i++) {
        getline(cin, line); // 形如 #a b# 或 #x y#
        next_char[(unsigned char)line[1]] = (unsigned char)line[2];
    }

    // 预处理跳转表:jump[j][c] = jump[j-1][ jump[j-1][c] ]
    for (int i = 0; i < MAXC; i++) jump[0][i] = next_char[i];
    for (int bit = 1; bit < LOG; bit++) {
        for (int i = 0; i < MAXC; i++) {
            jump[bit][i] = jump[bit - 1][jump[bit - 1][i]];
        }
    }

    int m;
    cin >> m;
    for (int query = 0; query < m; query++) {
        long long k;
        cin >> k;
        // 每个字符独立地完成 k 次替换:把 k 按二进制拆成若干 2^j 段
        string result;
        for (int i = 0; i < (int)initial.size(); i++) {
            int current = (unsigned char)initial[i];
            long long steps = k;
            int bit = 0;
            while (steps > 0) {
                if (steps & 1) current = jump[bit][current];
                steps >>= 1;
                bit++;
            }
            result.push_back((char)current);
        }
        cout << '#' << result << "#\n";
    }

    return 0;
}

复杂度

设初始字符串长度为 LL、查询数 mm、最大替换次数 KK

  • 时间:跳转表预处理 O(128logK)O(128 \log K);每次查询每个字符做 O(logK)O(\log K) 次跳转,总时间复杂度 O((128+mL)logK)O((128 + mL) \log K)
  • 空间:跳转表 O(128logK)O(128 \log K),字符串 O(L)O(L)

总结

重复应用同一个字符函数会形成周期,但不需要显式求周期。二进制倍增把 kk 次替换拆成若干 2j2^j 次替换,既能处理 10910^9 的大次数,又能为所有查询复用同一张字符跳转表。