在 AC 自动机上建立 max-plus 转移矩阵,快速幂求超长文章的最大匹配次数。
OJ: shumeng
题目 ID: CSP201509E
难度:提高+/省选-
标签:AC 自动机矩阵快速幂动态规划
日期: 2026-07-31 16:21
形式化题目
给定
思路
先看一个只适用于短文章的枚举基准:枚举每个位置选择哪个字母,写满长度后统计总出现次数。
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 自动机的状态。用
把每个单词插入 Trie,用 BFS 建好失配指针后,把子串的分数累加到父节点,这样走到某个状态时,score 就是“以当前位置结尾的所有重要单词数”,包含与重叠出现都被累计。
矩阵快速幂
长度
- 初始向量只有根状态为
,其余为负无穷; - 用二进制快速幂把转移矩阵自乘、按需乘到答案向量上,
步的最优值就是对所有状态取 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;
}复杂度
设自动机状态数为
- 时间:建自动机
,每次矩阵乘法 ,共 次,总时间 。 - 空间:存储矩阵与 Trie,
。
总结
超长长度使逐字符 DP 不可行,但转移规则固定。把普通加法换成“取最大值和加奖励”,构造 max-plus 意义下的矩阵,快速幂就能跳过任意多次相同转移。AC 自动机负责把“后缀匹配到哪个前缀”压缩成有限个状态,是这类“无限长度、有限状态”问题的标准组合。