最佳文章

在 AC 自动机上建立 max-plus 转移矩阵,快速幂求超长文章的最大匹配次数。

OJ: shumeng

题目 ID: CSP201509E

难度:提高+/省选-

标签:AC 自动机矩阵快速幂动态规划

日期: 2026-07-31 16:21

形式化题目

给定 nn 个单词和一个目标长度 mm,构造一个恰好 mm 个字符的文章,使得这 nn 个单词在文章中的总出现次数最大。出现可以重叠、可以互相包含,每次出现都单独计数。

思路

先看一个只适用于短文章的枚举基准:枚举每个位置选择哪个字母,写满长度后统计总出现次数。

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:58
 */
// brute.cpp:枚举所有小文章,并在追加字符时统计以当前位置结尾的单词。
#include <bits/stdc++.h>
using namespace std;

int n, length;
vector<string> words; // 重要单词列表
vector<char> letters; // 出现过的字母种类
string article;       // 当前构造中的文章
long long answer;

// 枚举第 position 个位置选择哪个字母;文章写满 length 个字母后更新答案。
void dfs(int position, long long score) {
    if (position == length) {
        answer = max(answer, score);
        return;
    }
    for (int i = 0; i < (int)letters.size(); i++) {
        article += letters[i];
        // 只统计“以当前位置结尾”的单词出现次数,避免重复计数。
        long long added = 0;
        for (int j = 0; j < n; j++) {
            int word_length = words[j].size();
            if ((int)article.size() >= word_length &&
                article.substr(article.size() - word_length) == words[j]) added++;
        }
        dfs(position + 1, score + added);
        article.pop_back();
    }
}

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

    cin >> n >> length;
    words.resize(n);
    int used[26] = {};
    for (int i = 0; i < n; i++) {
        cin >> words[i];
        for (int j = 0; j < (int)words[i].size(); j++) used[words[i][j] - 'a'] = 1;
    }
    for (int i = 0; i < 26; i++) if (used[i]) letters.push_back('a' + i);
    dfs(0, 0);
    cout << answer << '\n';
    return 0;
}

brute.cpp 用选择序列递归枚举整篇文章,每个位置尝试所有出现过的字母,时间复杂度为指数级,只适合小数据,但最忠实于题意。

DP 状态:AC 自动机

逐字符构造文章时,影响后续匹配的只有“当前文章后缀能匹配到的最长前缀”——这正是 AC 自动机的状态。用 dp[i]dp[i] 表示写到某一步、落在状态 ii 时能得到的最大重要度。

把每个单词插入 Trie,用 BFS 建好失配指针后,把子串的分数累加到父节点,这样走到某个状态时,score 就是“以当前位置结尾的所有重要单词数”,包含与重叠出现都被累计。

矩阵快速幂

长度 mm 高达 101510^{15},逐字符 DP 不可行,但状态转移是固定的。令矩阵 T[u][v]T[u][v] 表示“从状态 uu 追加一个字符到状态 vv 能增加的最大重要度”,在 max-plus 代数下(加法变取 max、乘法变加法)TkT^k 就表示走 kk 步的最优加分:

  • 初始向量只有根状态为 00,其余为负无穷;
  • 用二进制快速幂把转移矩阵自乘、按需乘到答案向量上,mm 步的最优值就是对所有状态取 max。

代码

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:58
 */
#include <bits/stdc++.h>
using namespace std;

const int MAXS = 105;
const long long NEG = -(1LL << 60); // 不可达状态,max 运算下的“负无穷”

// AC 自动机节点:next[c] 是字符 c 的转移,fail 是失配指针,
// score 是“以该状态结尾时能新增的匹配次数”。
struct Node {
    int next[26];
    int fail;
    int score;
};

// max-plus 意义下的转移矩阵:value[i][j] 表示从状态 i 走一步到 j 能得到的最大加分。
struct Matrix {
    long long value[MAXS][MAXS];
};

Node trie[MAXS]; // AC 自动机节点数组,下标 0 是根节点
int node_count = 1;

// 在 max-plus 代数下做矩阵乘法:C[i][j] = max_k (A[i][k] + B[k][j])。
Matrix multiply(const Matrix &left, const Matrix &right, int states) {
    Matrix result;
    for (int i = 0; i < states; i++) {
        for (int j = 0; j < states; j++) result.value[i][j] = NEG;
    }
    for (int i = 0; i < states; i++) {
        for (int k = 0; k < states; k++) {
            if (left.value[i][k] == NEG) continue;
            for (int j = 0; j < states; j++) {
                if (right.value[k][j] == NEG) continue;
                result.value[i][j] = max(result.value[i][j],
                                         left.value[i][k] + right.value[k][j]);
            }
        }
    }
    return result;
}

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

    int n;
    long long length;
    cin >> n >> length;

    // 把每个单词插入 Trie,单词末尾的 score 加 1。
    for (int i = 0; i < n; i++) {
        string word;
        cin >> word;
        int current = 0;
        for (int j = 0; j < (int)word.size(); j++) {
            int c = word[j] - 'a';
            if (trie[current].next[c] == 0) trie[current].next[c] = node_count++;
            current = trie[current].next[c];
        }
        trie[current].score++;
    }

    // BFS 构建失配指针,并把子串的分数累加到父节点:让包含关系与重叠出现都被计数。
    queue<int> q;
    for (int c = 0; c < 26; c++) {
        int child = trie[0].next[c];
        if (child != 0) q.push(child);
    }
    while (!q.empty()) {
        int u = q.front(); q.pop();
        trie[u].score += trie[trie[u].fail].score;
        for (int c = 0; c < 26; c++) {
            int v = trie[u].next[c];
            if (v != 0) {
                trie[v].fail = trie[trie[u].fail].next[c];
                q.push(v);
            } else {
                trie[u].next[c] = trie[trie[u].fail].next[c];
            }
        }
    }

    // 构造一步转移矩阵:从状态 i 沿字符 c 走到 v,获得 score[v] 分。
    Matrix transition;
    for (int i = 0; i < node_count; i++) {
        for (int j = 0; j < node_count; j++) transition.value[i][j] = NEG;
        for (int c = 0; c < 26; c++) {
            int v = trie[i].next[c];
            transition.value[i][v] = max(transition.value[i][v], (long long)trie[v].score);
        }
    }

    // best[i] 表示写出的前缀落在状态 i 时能得到的最大重要度,起点只能是根。
    long long best[MAXS] = {};
    for (int i = 1; i < node_count; i++) best[i] = NEG;
    // 二进制快速幂:把 transition 当作“走一步”,对 length 步做 max-plus 幂。
    while (length > 0) {
        if (length & 1) {
            long long next_best[MAXS];
            for (int j = 0; j < node_count; j++) next_best[j] = NEG;
            for (int i = 0; i < node_count; i++) {
                if (best[i] == NEG) continue;
                for (int j = 0; j < node_count; j++) {
                    next_best[j] = max(next_best[j], best[i] + transition.value[i][j]);
                }
            }
            for (int i = 0; i < node_count; i++) best[i] = next_best[i];
        }
        transition = multiply(transition, transition, node_count);
        length >>= 1;
    }
    long long answer = 0;
    for (int i = 0; i < node_count; i++) answer = max(answer, best[i]);
    cout << answer << '\n';
    return 0;
}

复杂度

设自动机状态数为 S100S \leqslant 100

  • 时间:建自动机 O(S26)O(S \cdot 26),每次矩阵乘法 O(S3)O(S^3),共 logm\log m 次,总时间 O(S3logm)O(S^3\log m)
  • 空间:存储矩阵与 Trie,O(S2+S26)O(S^2 + S \cdot 26)

总结

超长长度使逐字符 DP 不可行,但转移规则固定。把普通加法换成“取最大值和加奖励”,构造 max-plus 意义下的矩阵,快速幂就能跳过任意多次相同转移。AC 自动机负责把“后缀匹配到哪个前缀”压缩成有限个状态,是这类“无限长度、有限状态”问题的标准组合。