【笔记/模板】AC 自动机
AC 自动机
前缀函数以及 KMP 算法很好的处理了单个模式串和文本串之间的字符匹配问题,但是如果存在多个模式串对于单个文本串进行匹配时,每个模式串都要和文本串进行一次预处理,时间复杂度为 \(O(n (|T| + |S|))\),\(n\) 为模式串个数,\(|T|,|S|\) 为模式串和文本串长度。
而 AC 自动机很好的解决了这个问题,AC 自动机(Aho-Corasick Automaton)是一种同样利用了 KMP 思想,以 Tire 树为结构的多模式匹配算法。
算法过程
AC 自动机的过程分为两步:建立 Trie 树以及预处理失配指针。
建立 Trie 树
不多赘述,Trie 树上的每一个节点表示某一个模式串的前缀。
inline void insert(char *str)
{
int cur = 0, len = strlen(str);
for (int i = 0; i < len; i ++)
{
int k = str[i] - 'a';
if (!tr[cur][k]) tr[cur][k] = ++ idx;
cur = tr[cur][k];
}
}
处理失配指针
失配指针,又称为 \(fail\) 指针,本质即为 KMP 算法中的 \(next\) 辅助数组。
我们知道,KMP 算法中的 \(next[]\) 中记录着每一个前缀字符串的最长 \(border\) 值,AC 自动机的 \(fail\) 数组同样如此。\(fail_u\) 表示状态 \(u\) 的指针,指向着状态 \(u\) 的最大 border 值所处的状态(因为存在多个模式串,他们的 \(border\) 相互交叉),然而 Trie 树上每一个节点的状态刚好就是每一个模式串可能的所有前缀集合。
因此,AC 自动机的失配指针指向当前状态的最长后缀状态。
拿 OI-Wiki 的图举个例子:
节点 \(6\) 的状态为 \(\texttt{his}\),它的失配指针 \(fail_6 = 7\) 的状态为 \(\texttt{s}\),满足定义。
节点 \(9\) 的状态为 \(\texttt{she}\),它的失配指针 \(fail_9 = 2\) 的状态为 \(\texttt{he}\),满足定义。
那么如何构建呢?
首先一个显然的性质,一个 Trie 树上某一个节点的 \(fail\) 指针必然指向深度小于它的节点,因此可以使用 bfs 的框架。
其次,就像我们在 KMP 预处理 \(next_1 \gets 0\) 一样,Trie 树的第二层(根节点为第一层)不存在匹配,因此直接指向 \(0\)。
剩下的每一层,不难发现会有继承关系,就像上图所示的 \(fail_8 = 1, fail_9 = 2\),通过前缀函数定义显然易证。因此让每一个当前节点的 fail 指向它的父亲节点的 fail 指针的这一个下标,代码语言可以写为:
cur -> son[k] -> fail = cur -> fail -> son[k];
以上是在有这个节点的情况下,如果不存在而不去处理,后面的 bfs 的 \(fail\) 数组很可能指向这里,因此我们还要把不存在的这个节点直接当作之前所说的哪一个节点,即:
cur -> son[k] = cur -> fail -> son[k];
像这样,后面的 \(fail\) 如果跳到了 \(cur\) 上的 \(son_k\),就会直接跳向 \(fail_{cur}\) 上的 \(son_k\) 了。
之所以不用考虑边界问题,是因为不存在的默认置 \(0\) 了。
inline void build()
{
hh = 0, tt = -1;
for (int k = 0; k < 26; k ++)
if (tr[0][k]) fail[tr[0][k]] = 0, que[++ tt] = tr[0][k];
while (hh <= tt)
{
int cur = que[hh ++];
for (int k = 0; k < 26; k ++)
{
if (tr[cur][k]) fail[tr[cur][k]] = tr[fail[cur]][k], que[++ tt] = tr[cur][k];
else tr[cur][k] = tr[fail[cur]][k];
}
}
}
多模式匹配
对于查询的文本串 \(T\),我们只需要在处理好的 Trie 树上走一遍,对于每一个走到的状态,这个状态都是 \(T\) 的前缀,因此这个状态和包括所有的 \(fail\) 指针都匹配上了 \(T\),按照要求处理即可。
比如 P3808 AC 自动机(简单版) - 洛谷 | 计算机科学教育新生态 中的查询:
int query(char *str)
{
int cur = 0, ans = 0, len = strlen(str);
for (int i = 0; i < len; i ++)
{
int k = str[i] - 'a';
cur = tr[cur][k];
for (int temp = cur; temp && ~cnt[temp]; temp = fail[temp])
ans += cnt[temp], cnt[temp] = -1;
}
return ans;
}
P3796 AC 自动机(简单版 II) - 洛谷 | 计算机科学教育新生态 中:
void query(char *str)
{
int cur = 0, len = strlen(str);
for (int i = 0; i < len; i ++)
{
cur = tr[cur][str[i] - 'a'];
for (int temp = cur; temp; temp = fail[temp])
ans[rec[temp]].cnt ++;
}
}
实际上这样的复杂度是错误的,和 KMP 算法中的一样,不断地跳 \(next\) 时间复杂度为 \(O(|S|)\),因此此时的 AC 自动机时间复杂度达到了 \(O(\textstyle{\sum}|S| \times |T|)\),因此我们需要优化。
效率优化
之前说到,一个状态的 \(fail\) 深度小于这个状态的深度,又由于每一个点只有一个 \(fail\) 指针,每个点互相之间应当联通,因此可以证明:
Trie 树上只保留 \(fail\) 的情况下是一棵树,根节点为 \(0\)。
因此暴力跳 \(fail\) 的过程变为了在一棵树上求和的过程,用 拓扑排序 或者 bfs 都可以。
P5357 【模板】AC 自动机 - 洛谷 | 计算机科学教育新生态 Code:
struct ACAutomaton
{
int tr[N][26], fail[N], idx;
int que[N], hh = 0, tt = -1;
inline void insert(char *str, int id)
{
int cur = 0, len = strlen(str);
for (int i = 0; i < len; i ++)
{
int k = str[i] - 'a';
if (!tr[cur][k]) tr[cur][k] = ++ idx;
cur = tr[cur][k];
}
rec[id] = cur;
}
inline void build()
{
hh = 0, tt = -1;
for (int k = 0; k < 26; k ++)
if (tr[0][k]) fail[tr[0][k]] = 0, que[++ tt] = tr[0][k];
while (hh <= tt)
{
int cur = que[hh ++];
for (int k = 0; k < 26; k ++)
{
if (tr[cur][k]) fail[tr[cur][k]] = tr[fail[cur]][k], que[++ tt] = tr[cur][k];
else tr[cur][k] = tr[fail[cur]][k];
}
}
for (int ver = 1; ver <= idx; ver ++) add(fail[ver], ver);
}
void query(char *str)
{
int cur = 0, len = strlen(str);
for (int i = 0; i < len; i ++)
{
cur = tr[cur][str[i] - 'a'];
sum[cur] ++;
}
}
} AC;
void dfs(int ver)
{
for (int i = h[ver], to = e[i]; ~i; i = ne[i], to = e[i])
{
dfs(to);
sum[ver] += sum[to];
}
}
Reference
P3808 AC 自动机(简单版) - 洛谷 | 计算机科学教育新生态
P3796 AC 自动机(简单版 II) - 洛谷 | 计算机科学教育新生态
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具