AC自动机
AC 自动机
前言
我觉得AC自动机这种东西非常抽象,有必要写一篇博客来整理一下,以加深理解。
概况
AC自动机是以 Trie 树的结构为基础,结合 KMP 思想建立的自动机,用于解决多模式串匹配等任务。
一般来说,建立一个AC自动机有两个步骤:
- 把所有的模式串建成一颗 Trie 树。
- 用 KMP 的思想对 Trie 树上所有的节点构建失配指针。
字典树构建
字典树即 Trie 树,它的构建不必多说。拿 ABC、BCD、BD、C 这四个字符串举个例子,构建出来的 Trie 树就是这个样子(如图1),为了方便,我们把每个节点所代表的字符作为它的入度的边的边权。
图1
void insert(char s[]) {
int u = 1, len = strlen(s);
for (int i = 0; i < len; i++) {
int v = s[i] - 'a';
if (!trie[u][v]) trie[u][v] = ++tot;
u = trie[u][v];
}
cnt[u]++; // 具体看情况
}
失配指针
含义
失配指针即 fail 指针,我们记
构建
首先我们显然可以知道,一个节点的 fail 指针指向的点的深度一定是比该点要小的。所以
设点
- 若存在节点
,则 。还是拿图1举例子,现在以求出 ,那么 即 。 - 若不存在节点
(即 ),则 。依旧是拿图1举例子,显然节点 不存在,而 和 所代表的字符串(后缀)是相同的,所以我们从 向 连一条边,即从 向 连一条边,这样节点 就存在了。 - 因为要求出父亲节点的 fail 再求儿子的 fail,所以我们用广搜来实现。
然后我们的 trie 树(图)就会变成这个样子,如图2。虽然很抽象,但凑合着还能看。
图2
void getfail() {
for (int i = 0; i < 26; i++) trie[0][i] = 1; // 初始化0的所有儿子都是1
q.push(1); // 将根压入队列
fail[1] = 0;
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = 0; i < 26; i++) { // 遍历所有儿子
if (!trie[u][i]) { // 如果不存在该节点,对应 2
trie[u][i] = trie[fail[u]][i]; // 从u向trie[fail[u]][i]连一条边
continue;
}
fail[trie[u][i]] = trie[fail[u]][i]; // 求fail,对应 1
q.push(trie[u][i]); // 将实点压入队列
}
}
}
怎么用
这还用说吗?看着用呗。
代码
拿例题来说事儿吧。[HDU 2222]Keywords Search
#include <bits/stdc++.h>
using namespace std;
const int N = 1e4 + 10, M = 1e6 + 10;
int n, t;
char s[M];
int trie[M][26], cnt[M], flag[M], fail[M];
int tot;
queue<int> q;
void insert(char s[]) { // 构建trie树
int u = 1, len = strlen(s);
for (int i = 0; i < len; i++) {
int v = s[i] - 'a';
if (!trie[u][v]) trie[u][v] = ++tot;
u = trie[u][v];
}
cnt[u]++;
}
void getfail() {
for (int i = 0; i < 26; i++) trie[0][i] = 1; // 初始化0的所有儿子都是1
q.push(1); // 将根压入队列
fail[1] = 0;
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = 0; i < 26; i++) { // 遍历所有儿子
if (!trie[u][i]) { // 如果不存在该节点,对应 2
trie[u][i] = trie[fail[u]][i]; // 从u向trie[fail[u]][i]连一条边
continue;
}
fail[trie[u][i]] = trie[fail[u]][i]; // 求fail,对应 1
q.push(trie[u][i]); // 将实点压入队列
}
}
}
int query(char s[]) {
int u = 1, len = strlen(s), ans = 0;
for (int i = 0; i < len; i++) {
int v = s[i] - 'a';
int k = trie[u][v];
while (k > 1 && flag[k] != -1) {
ans = ans + cnt[k];
flag[k] = -1;
k = fail[k]; // 如果当前节点可以匹配成功的话,那么它的fail一定也可以
}
u = trie[u][v];
}
return ans;
}
void solve() {
memset(trie, 0, sizeof(trie));
memset(flag, 0, sizeof(flag));
memset(fail, 0, sizeof(fail));
memset(cnt, 0, sizeof(cnt));
tot = 1; // 这十分重要
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%s", s); // 读入模式串
insert(s); // 把模式串扔到trie树里
}
getfail();
scanf("%s", s); // 读入文本串
printf("%d\n", query(s)); // 拿文本串查询
}
int main() {
scanf("%d", &t);
while (t--) {
solve();
}
return 0;
}
AC 自动机上 DP
懵了吧?因为 AC 自动机本质上是 trie 树,是一棵树,所以他自然是可以 dp 的。既然它是一棵树,那就可以进行更多的树上操作,比如树剖。
AC 自动机上 dp 一般比较套路。
然后就没了。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】