AC 自动机

(如果学了 KMP 会使得 AC 自动机好理解一点吗?或许是的,不过我是用 AC 自动机来理解 KMP 的。KMP 可以看做单串 AC 自动机,建出来的自动机是一条链,但是其 Fail 树变成了一个叫失配树的东西,跟 Fail 树原理一样的。)

对于字符串问题,很多都跟子串相关,对于长度为 \(n\) 的字符串,子串数量是 \(n^2\) 级别的。所以就产生了(主要的)两种思想解决子串问题:

1.子串可以表示为一个后缀的前缀

2.子串可以表示为一个前缀的后缀

其中第一个思想带来了后缀相关算法,第二个思想带来了 AC 自动机。

后缀相关算法会在以后写博客。这里只讲 AC 自动机。

先考虑建立一个串的 AC 自动机及其 Fail 树。

把这个串插入字典树中,这是初始形态。对于自动机,有一个规定是每个当前状态后面加入一个字符,都能找到后续状态。所以对于字典树上每个点其它空儿子,找到最长的一个前缀等于当前后缀(包含到空儿子的边的字符)的前缀节点,把这个空儿子设为那个前缀节点。后面会说怎么建。

此时对于每个点,代表的是插入字符串的一个前缀。而在上面的一条从根开始的路径(可能不是简单路径),代表一个字符串。这个路径的终点,就是当前最长前缀与这个路径所对应的字符串的后缀相等。

先搞清楚 \(Fail\) 的意思:\(Fail_u\) 表示自动机上 \(u\) 节点所代表的前缀的最长前缀等于当前后缀的前缀节点。在多串自动机上,这个最长前缀等于当前后缀的最长前缀可以是其它串的前缀。

建立自动机时,考虑从根开始 BFS 构建。考虑已知当前节点 \(u\)\(Fail\)\(Next_{u,ch}\) 表示在当前字符串的末尾加一个字符 \(ch\) 得到的新字符串在字典树上的节点编号。如果不存在,那么由于自动机必须要求存在这样的一个节点,所以只能往前找到最长的一个前缀满足那个前缀跟当前后缀相同,且那个前缀(假设为 \(v\) 号点)的 \(Next_{v,ch}\) 存在。由于是 BFS 从上往下构建的,\(v\) 又一定在 \(u\) 上方(多串时可能不是祖先,但一定深度比 \(u\) 浅),所以 \(Next_{v,ch}\) 一定都是存在的,就算原先不存在,也会在处理 \(v\) 节点时被更新。而“最长的一个前缀满足那个前缀跟当前后缀相同”就是 \(Fail\) 的定义,所以当 \(Next_{u,ch}\) 在字典树上不存在时,\(Next_{u,ch}=Next_{Fail_u,ch}\)

如果 \(Next_{u,ch}\) 存在,则考虑计算 \(Fail_{Next_{u,ch}}\),然后再把 \(u\) 节点放入队列里。发现上文中若 \(Next_{u,ch}\) 不存在时找到的代替节点,在 \(Next_{u,ch}\) 存在时则就是 \(Fail_{Next_{u,ch}}\),通过 \(Fail\) 的定义即可证明。

感性地理解,\(Fail\) 有种路径压缩的感觉。把 AC 自动机的形态画出来,主体部分为字典树,然后会多一堆返祖边。把 Fail 树的形态画下来,每个点的父亲相当于这个点代表的前缀的最长前缀等于当前后缀的前缀。

注意,上文说的最长前缀等于当前后缀的前缀必须是真前缀,也就是不能为原串本身。

单串 AC 自动机的 Fail 树被称作失配树,跟多串 AC 自动机的 Fail 树是一样的东西,所以没什么好说的。考虑 KMP 匹配,本质上就是在 AC 自动机上走一条路径。然后某一步碰到字典树最底下的节点,说明当前的路径代表的字符串的后缀匹配到了模式串。

多串 AC 自动机的构造过程跟单串相同。把每个模式串的末尾节点在 AC 自动机上的位置标出来,设为关键点。在字典树插入时,对于每一次插入,在这次插入的末尾节点的 \(Cnt\) 增加 \(1\)。当前 \(Cnt_u\) 则表示 \(u\) 这个点代表的前缀跟多少个模式串恰好相等。如果把 Fail 树建出来,吗,设 \(Sum_u\) 表示从根到 \(u\) 的链上所有 \(Cnt\) 的和,则 \(Sum_u\) 表示当前节点所代表的前缀包含多少个模式串,包含指恰好相等或者为其后缀。这里的包含跟普通包含是有区别的。因为如果不是其后缀,而是中间的一个子串,那么在前面的某个时刻,这个子串会是前面那个时刻的前缀的后缀。而想要到现在这个前缀,肯定会经过前面那个前缀,所以中间的子串一定会被算到,只需要计算当前前缀的后缀新产生的能够匹配的模式串即可。

\(Sum\) 可以单独建 Fail 树求,也可以在建自动机的时候求 Fail 的时候顺便求了。

想要知道给定文本串包含多少模式串,就在 AC 自动机上按照这个文本串走一条路径,每一步就把当前节点的 \(Sum\) 加上即可。对于一些题目要求找到文本串使得包含的模式串最多、最少,或者其它跟模式串和文本串相关问题,可以建立 dp 状态 \(f_{i,j}\) 表示当前文本串长度为 \(i\),当前在自动机的 \(j\) 号点的答案,然后枚举下一步走的点。这就是 AC 自动机上的 \(dp\)

特别地,一些 AC 自动机上的 dp 不存在长度那个维度,所以可能存在环。可以通过高斯消元把环消掉得出答案。

有一类问题跟 AC 自动机的关系不大,主要在 Fail 树上操作。Fail 树有两个最重要的操作。

如果要查询所有模式串在某一个模式串中出现了多少次:

1.可以枚举这个模式串的所有前缀在 Fail 树上的节点,这个前缀产生的贡献为 \(Sum_u\)。答案为所有 \(Sum_u\) 的和。这又可以分为两种求法:对于所有模式串的最后一个节点,立刻把其子树内的点都 \(+1\),修改 \(O(log)\),查询单点 \(O(log)\),或树上差分,需要离线,所有修改加起来 \(O(n)\),查询单点 \(O(1)\)

2.把询问串的所有点在 Fail 树上的位置标记下来,枚举所有模式串,这个模式串最后一个节点的子树内有多少个标记,即为这个模式串产生的贡献。

这两种方法都是基于,Fail 树上的父亲都是后缀子串,而自己永远都是子树内其它点的后缀。处理子串相关问题会利用到这个性质。

大概写完了。后续有补充的会直接加在后面。

posted @ 2023-07-17 10:42  0htoAi  阅读(52)  评论(0编辑  收藏  举报