1246

用 KMP 统计替换块交界贡献,利用六周期前后缀状态做仿射矩阵快速幂。

OJ: shumeng

题目 ID: CSP202006D

难度:省选/NOI-

标签:字符串KMP矩阵快速幂递推

日期: 2026-07-31 16:21

形式化题目

从字符串 1 开始,每秒每个数字独立替换为 2 的幂:

12,24,416,664 1\to2,\quad 2\to4,\quad 4\to16,\quad 6\to64。

给出轮数 nn 和模式串 SS,求第 nn 秒字符串中 SS 的出现次数,对 998244353998244353 取模。

思路

直接展开字符串会指数爆炸,必须从替换结构上统计。令 F[k][a]F[k][a] 表示模式串在字母 aa 的第 kk 轮展开中的出现次数。展开后的每次出现要么完全位于某个子展开内部,要么跨越两个相邻子展开的边界。

跨边界匹配

跨边界匹配只需要左块长度 S1|S|-1 的后缀和右块同长度的前缀。用 KMP 在它们的拼接串中统计跨边界匹配数。四种字母的这些截断前后缀经过若干轮后以 6 为周期重复,程序比较当前状态和 6 步前的状态来确定预热结束位置。

矩阵快速幂

每一轮的 FF 是四维线性转移加上跨边界常数项。加入常数维度后写成五维矩阵;把周期内六个矩阵相乘,再对它快速幂即可跳过极大的 nn。模式长度为 1 时不存在跨边界项,直接使用替换矩阵。

先看一个小数据暴力解,直接展开整个字符串再逐位置统计:

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:小数据暴力解,小轮数直接展开整个字符串,再逐位置比较模式串。
#include <bits/stdc++.h>
using namespace std;

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

    int n;
    string pattern, text = "1";
    cin >> n >> pattern;
    // 模拟每一秒:每个数字独立替换成 2 的幂,串长指数增长
    while (n--) {
        string next;
        for (int i = 0; i < (int)text.size(); i++) {
            if (text[i] == '1') next += '2';
            else if (text[i] == '2') next += '4';
            else if (text[i] == '4') next += "16";
            else next += "64";
        }
        text.swap(next);
    }
    // 逐位置统计模式串出现次数
    int answer = 0;
    for (int i = 0; i + (int)pattern.size() <= (int)text.size(); i++) {
        if (text.compare(i, pattern.size(), pattern) == 0) answer++;
    }
    cout << answer << '\n';

    return 0;
}

代码

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 long long MOD = 998244353;
const int STATE = 4;    // 字符种数:1,2,4,6
const int MATRIX = 5;   // 状态向量多一维常数项,便于在矩阵里加入跨边界项

string pattern;
int pattern_length, limit_length;   // limit_length = 模式串长度 - 1
string replace_text[STATE] = {"2", "4", "16", "64"};
string alphabet = "1246";
int transition[STATE][STATE];       // 替换矩阵:字母 a 的一次替换产生的各子字母数
vector<int> fail;                   // 模式串的 KMP 失配数组

// 某一时刻下各字母展开串截断后的前缀与后缀(长度不超过 limit_length)
struct TextState {
    string prefix[STATE], suffix[STATE];
};

// 用于快速幂的 5 阶转移矩阵
struct Matrix {
    long long value[MATRIX][MATRIX];
};

// 字符到下标:'1','2','4','6' -> 0,1,2,3
int get_id(char ch) {
    if (ch == '1') return 0;
    if (ch == '2') return 1;
    if (ch == '4') return 2;
    return 3;
}

// 构建模式串的 KMP 失配数组
void build_kmp() {
    fail.assign(pattern_length, 0);
    for (int i = 1; i < pattern_length; i++) {
        int position = fail[i - 1];
        while (position > 0 && pattern[position] != pattern[i]) position = fail[position - 1];
        if (pattern[position] == pattern[i]) position++;
        fail[i] = position;
    }
}

// 统计 left + right 拼接串中跨越边界 boundary 的完整模式匹配个数
int count_cross(const string &left, const string &right) {
    string text = left + right;
    int answer = 0, matched = 0, boundary = (int)left.size();
    for (int i = 0; i < (int)text.size(); i++) {
        while (matched > 0 && pattern[matched] != text[i]) matched = fail[matched - 1];
        if (pattern[matched] == text[i]) matched++;
        if (matched == pattern_length) {
            int start = i - pattern_length + 1;
            if (start < boundary && start + pattern_length > boundary) answer++;
            matched = fail[matched - 1];
        }
    }
    return answer;
}

// 由当前时刻的各字母前后缀,推出下一时刻的前后缀
TextState next_state(const TextState &current) {
    TextState next;
    for (int a = 0; a < STATE; a++) {
        for (int i = 0; i < (int)replace_text[a].size(); i++) {
            int child = get_id(replace_text[a][i]);
            if ((int)next.prefix[a].size() < limit_length) {
                next.prefix[a] += current.prefix[child];
                if ((int)next.prefix[a].size() > limit_length) next.prefix[a].resize(limit_length);
            }
            next.suffix[a] += current.suffix[child];
            if ((int)next.suffix[a].size() > limit_length) {
                next.suffix[a] = next.suffix[a].substr(next.suffix[a].size() - limit_length);
            }
        }
    }
    return next;
}

// 两个时刻的前后缀状态是否完全相同(用于判断六周期)
bool same_state(const TextState &left, const TextState &right) {
    for (int i = 0; i < STATE; i++) {
        if (left.prefix[i] != right.prefix[i] || left.suffix[i] != right.suffix[i]) return false;
    }
    return true;
}

// 走一轮替换:每种字母的出现次数由上一轮线性转移加上跨边界匹配数
array<long long, STATE> step(const array<long long, STATE> &old, const array<long long, STATE> &cross) {
    array<long long, STATE> next = {};
    for (int a = 0; a < STATE; a++) {
        next[a] = cross[a];
        for (int b = 0; b < STATE; b++) next[a] = (next[a] + 1LL * transition[a][b] * old[b]) % MOD;
    }
    return next;
}

// 矩阵乘法
Matrix multiply(const Matrix &left, const Matrix &right) {
    Matrix result = {};
    for (int i = 0; i < MATRIX; i++) {
        for (int k = 0; k < MATRIX; k++) {
            if (left.value[i][k] == 0) continue;
            for (int j = 0; j < MATRIX; j++) {
                result.value[i][j] = (result.value[i][j] + left.value[i][k] * right.value[k][j]) % MOD;
            }
        }
    }
    return result;
}

// 矩阵快速幂
Matrix power(Matrix base, long long exponent) {
    Matrix result = {};
    for (int i = 0; i < MATRIX; i++) result.value[i][i] = 1;
    while (exponent) {
        if (exponent & 1) result = multiply(result, base);
        base = multiply(base, base);
        exponent >>= 1;
    }
    return result;
}

// 矩阵乘状态向量(最后一维固定为 1,表示常数维)
array<long long, MATRIX> apply_matrix(const Matrix &matrix, const array<long long, MATRIX> &vector_value) {
    array<long long, MATRIX> result = {};
    for (int i = 0; i < MATRIX; i++) {
        for (int j = 0; j < MATRIX; j++) result[i] = (result[i] + matrix.value[i][j] * vector_value[j]) % MOD;
    }
    return result;
}

// 由当前轮的跨边界项构造一轮转移矩阵(含常数维)
Matrix make_matrix(const array<long long, STATE> &cross) {
    Matrix result = {};
    for (int i = 0; i < STATE; i++) {
        for (int j = 0; j < STATE; j++) result.value[i][j] = transition[i][j];
        result.value[i][STATE] = cross[i];
    }
    result.value[STATE][STATE] = 1;
    return result;
}

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

    long long n;
    cin >> n >> pattern;
    pattern_length = (int)pattern.size();
    // 预处理替换矩阵 transition[a][b]
    for (int a = 0; a < STATE; a++) {
        for (int i = 0; i < (int)replace_text[a].size(); i++) transition[a][get_id(replace_text[a][i])]++;
    }

    array<long long, STATE> initial = {};
    if (pattern_length == 1) {
        // 单字符模式:不存在跨边界项,直接对纯替换矩阵快速幂
        for (int i = 0; i < STATE; i++) initial[i] = pattern[0] == alphabet[i];
        array<long long, STATE> zero_cross = {};
        Matrix plain = make_matrix(zero_cross);
        array<long long, MATRIX> vector_value = {initial[0], initial[1], initial[2], initial[3], 1};
        cout << apply_matrix(power(plain, n), vector_value)[0] << '\n';
        return 0;
    }

    build_kmp();
    limit_length = pattern_length - 1;
    TextState current;
    for (int i = 0; i < STATE; i++) current.prefix[i] = current.suffix[i] = string(1, alphabet[i]);
    vector<array<long long, STATE> > cross_list;   // 每轮的跨边界项
    vector<array<long long, STATE> > count_list(1, initial);   // 每轮的 F 值,count_list[0] 为第 0 轮
    deque<TextState> recent;
    recent.push_back(current);

    // 逐轮推进,直到连续 6 轮的前后缀状态重复(六周期)
    int time = 0, begin = -1;
    while (true) {
        array<long long, STATE> cross = {};
        // 跨边界匹配只可能出现在 4 -> 16 和 6 -> 64 这两处替换
        cross[2] = count_cross(current.suffix[0], current.prefix[3]);
        cross[3] = count_cross(current.suffix[3], current.prefix[2]);
        cross_list.push_back(cross);
        count_list.push_back(step(count_list.back(), cross));
        current = next_state(current);
        time++;
        recent.push_back(current);
        if ((int)recent.size() == 7) {
            if (same_state(recent.front(), recent.back())) {
                begin = time - 6;
                break;
            }
            recent.pop_front();
        }
    }
    if (n <= time) {
        cout << count_list[n][0] << '\n';
        return 0;
    }

    // 把周期内 6 个矩阵连乘,再快速幂跳过大量轮数
    Matrix period = {};
    for (int i = 0; i < MATRIX; i++) period.value[i][i] = 1;
    for (int i = 0; i < 6; i++) period = multiply(make_matrix(cross_list[begin + i]), period);
    array<long long, MATRIX> vector_value = {count_list[begin][0], count_list[begin][1], count_list[begin][2], count_list[begin][3], 1};
    long long repeat = (n - begin) / 6;
    vector_value = apply_matrix(power(period, repeat), vector_value);
    // 剩余不足一个周期的轮数逐轮处理
    int remain = (int)((n - begin) % 6);
    for (int i = 0; i < remain; i++) vector_value = apply_matrix(make_matrix(cross_list[begin + i]), vector_value);
    cout << vector_value[0] << '\n';

    return 0;
}

复杂度

设模式串长度为 mm,周期前的轮数为 K=O(logm)K=O(\log m)。KMP 统计前后缀交界耗时 O(Km)O(Km),矩阵跳转耗时 O(logn)O(\log n),空间复杂度为 O(m)O(m)

总结

巨大替换串不必展开。把出现次数分为子块内部和块间交界两部分,再将有限前后缀状态的周期性转化为矩阵跳转,就能处理 10910^9 轮替换。

图示解析

text
h^k(a) 的模式出现次数
|- 子块内部:由 F[k] 线性相加
`- 相邻子块边界:suffix[k] + prefix[k] 的 KMP 匹配
   `- 前后缀状态六周期,合并为矩阵快速幂

交界计数补上了普通替换计数遗漏的匹配;前后缀只保留模式长度附近,因此不会随完整展开串增长。