Aho-Corasick automaton,该算法在1975年产生于贝尔实验室,是著名的多模匹配算法。
前言
ZR的rxd老师加大力度讲了一发AC
自动机,凭借着KMP自动机没有自闭的光环,我居然没 有 自 闭!
在晚上自习时重新看了一遍黄队的博客并做了几道题之后,我对AC自动机有了更深的了解,所以来这里瞎扯几句,以便之后复习.
参考资料
前置知识
Trie树(字典树
不知为啥rxd叫他字母树)KMP单字符串匹配算法
很多神仙都说过一个公式:
Aho-Corasick automaton = Trie + KMP
没错,AC自动机正是以Trie
的结构为基础结合KMP
的思想建立的。
建立
AC自动机是一个用来求解多模式串匹配问题的算法.
建立一个AC自动机通常需要两步:
- 一棵
Trie
的结构,把所有的模式串插入Trie
- KMP单字符串匹配算法的思想,对
Trie
上的节点建立失配指针
Trie构建
和普通的Trie
没有什么区别…,直接写一个Trie
的Insert
操作即可.
注意:这里Trie
上的每一个节点是一个模式串的前缀.为了简便,我们将在下文把他称为一个状态(此处借用OI-wiki
的约定),Trie
的每一个节点表示一个状态,Trie
的边可以看作状态的转移(这怎么有点像dp)
我们把所有这些状态组成的集合称为$U$
因为我们只利用字典树的结构,所以只用把模式串插入Trie
即可.
构造失配指针
AC自动机利用一个失配指针failed
来辅助匹配
状态(节点)u
的failed
指针指向状态v
,且$v \in U$,并且v
是u
的最长后缀.
注意:这里的failed
和KMP算法中的next
指针的不同点在于next
指针求的是最长的Border
(也就是最长前后缀),而failed
是最长后缀.
清楚不同点之后,我们利用KMP
的思想来构建failed
指针:
我们通过一遍BFS
来构建,假设现在我们BFS
到的节点为now
,他的父亲为fa
,fa
通过字符str
转移到now
上,即Trie[fa][str] = now
.
由于这是BFS
,那么深度小于now
的failed
指针都已经被求出(这自然包括fa
)
我们首先查询fa
的failed
指针:
如果
Trie[failed[fa]][str]
指向的值w
存在,那么我们就让failed[now] = w
.如果上式并不存在,那么我们继续跳
failed
,即找到failed[failed[p]]
,如果还是不存在,那么继续跳,直到找到满足的状态为止.如果跳到根节点也没有找到,那么直接令
failed[now]
等于根节点
这里有一张来自OI-wiki的gif,帮助大家理解.
Trie图与AC自动机的优化
我们发现,在上述过程中的暴力跳failed
这一段时间复杂度不太优秀,那么我们引入Trie
图来优化.
我们考虑在当前节点的父亲的failed
指针无法通过str
字符进行转移的情况下,可以通过修改Trie
树的结构来压缩状态.
我们考虑状态u
,如果在u
后面加上一个字符str
向下转移成下一个状态v
(我们记这个东西为u' = trans(u,str)
).
如果
v
存在,那么一定有一个模式串的前缀为v
.如果
v
不存在,那么我们让trans(u,str)
指向trans(failed[u],str)
对于上一个东西的解释:因为
failed[u]
是状态u
的后缀,那么显然trans(failed[u],str)
就是u'
的后缀
这里还是有一张来自OI-wiki
的gif,帮助大家理解
可以发现,这些黑色的边将Trie
树变成了Trie
图,通过压缩状态简化了跳failed
的时间
时间复杂度
AC 自动机的时间复杂度在需要找到所有匹配位置时是$O(|s|+m)$, $|s|$是文本串长度,$m$表示模板串的总匹配次数;
实现
我们以LuoguP3808为例.
1 | // luogu-judger-enable-o2 |