把所有模式串插入 Trie 并建立 fail 指针,扫描文本串时沿 fail 链统计出现过的终止节点。
OJ: luogu
题目 ID: P3808
难度:普及/提高-
标签:字符串字典树AC自动机模板题
日期: 2026-07-06 23:57
题意
给定 n 个模式串和一个文本串 t,要求统计有多少个模式串在 t 中出现过。
注意:题目按“编号”区分模式串。
如果两个模式串内容完全相同,但编号不同,只要这个字符串在文本中出现,它们都要分别计数。
思路
最直接的做法,是逐个模式串去文本串里找。
下面这个暴力按编号逐个检查,所以重复模式串也能正确计数:
// brute.cpp:小数据暴力解,逐个模式串检查它是否在文本串中出现。
#include <bits/stdc++.h>
using namespace std;
int n;
string pattern_list[105];
string text;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> pattern_list[i];
}
cin >> text;
int ans = 0;
for (int i = 1; i <= n; i++) {
bool found = false;
int len = (int)pattern_list[i].size();
for (int l = 0; l + len <= (int)text.size(); l++) {
bool same = true;
for (int j = 0; j < len; j++) {
if (text[l + j] != pattern_list[i][j]) {
same = false;
break;
}
}
if (same) {
found = true;
break;
}
}
if (found) ans++;
}
cout << ans << "\n";
return 0;
}但总模式串长度和文本串长度都能达到 10^6,逐个查找会超时。
我们需要一次扫描文本串,同时匹配所有模式串。这正是 AC 自动机的用途。
第一步:建 Trie
把所有模式串插入 Trie。
每个终止节点记录:
end_count[u] = 有多少个模式串编号在 u 这个节点结尾如果两个模式串内容相同,它们会落在同一个终止节点,但 end_count[u] 会累加两次。
第二步:建 fail 指针
fail[u] 表示:当前状态匹配失败时,应该跳到哪个最长后缀状态继续匹配。
用 BFS 建 fail 指针时,同时补全缺失转移:
- 如果
u有字符c的儿子,就给这个儿子设置 fail; - 如果没有,就让
trie[u][c] = trie[fail[u]][c]。
这样扫描文本串时,每个字符都能直接转移到下一个状态。
第三步:扫描文本串
扫描到文本串某个位置时,当前状态 u 表示“以当前位置结尾的某个后缀”。
沿着 u 的 fail 链向上走,链上的所有终止节点,都代表一个以当前位置结尾的模式串。
统计后把节点标记为 -1,表示这个终止节点已经贡献过答案。这样同一个模式串编号即使在文本中出现多次,也只会被统计一次。
代码
#include <bits/stdc++.h>
using namespace std;
const int MAXNODE = 1000005;
const int SIGMA = 26;
int n;
int trie[MAXNODE][SIGMA];
int fail_link[MAXNODE];
int end_count[MAXNODE]; // end_count[u] 表示有多少个模式串在 u 结尾
int node_cnt;
string text;
void insert_pattern(const string &s) {
int u = 0;
for (int i = 0; i < (int)s.size(); i++) {
int c = s[i] - 'a';
if (trie[u][c] == 0) {
node_cnt++;
trie[u][c] = node_cnt;
}
u = trie[u][c];
}
end_count[u]++;
}
void build_ac_automaton() {
queue<int> q;
for (int c = 0; c < SIGMA; c++) {
int v = trie[0][c];
if (v != 0) {
fail_link[v] = 0;
q.push(v);
}
}
while (!q.empty()) {
int u = q.front();
q.pop();
for (int c = 0; c < SIGMA; c++) {
int v = trie[u][c];
if (v != 0) {
fail_link[v] = trie[fail_link[u]][c];
q.push(v);
} else {
// 补全转移:失配时直接跳到 fail 后能走到的状态。
trie[u][c] = trie[fail_link[u]][c];
}
}
}
}
long long count_matched_patterns() {
long long ans = 0;
int u = 0;
for (int i = 0; i < (int)text.size(); i++) {
int c = text[i] - 'a';
u = trie[u][c];
// 沿 fail 链统计所有以当前位置结尾的模式串。
// 统计过的节点标成 -1,保证同一个模式串编号只贡献一次。
int p = u;
while (p != 0 && end_count[p] != -1) {
ans += end_count[p];
end_count[p] = -1;
p = fail_link[p];
}
}
return ans;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; i++) {
string s;
cin >> s;
insert_pattern(s);
}
cin >> text;
build_ac_automaton();
cout << count_matched_patterns() << "\n";
return 0;
}复杂度
设所有模式串总长度为 L,文本串长度为 T。
建 Trie、建 fail 指针和扫描文本串都在总长度量级内完成;补全转移时每个节点枚举 26 个小写字母。
时间复杂度:
空间复杂度:
总结
AC 自动机可以理解为:
Trie + fail 指针Trie 负责保存所有模式串,fail 指针负责在匹配失败时跳到最长可用后缀。
本题还要特别注意“按编号计数”:重复字符串不能去重,而是要在终止节点累计数量。