首先,AC自动机应该是一个FA,即有限状态自动机。
任意一个有限状态自动机
M
都是一个五元组。
Q
是有限大小的状态集合,
δ
是转移函数,是一个状态集合到状态集合自身的对应关系。
q0
是初始状态,
F
是结束状态集合。
那么对于AC自动机而言,它是以trie树为基础,并以trie树结点为它的状态的自动机,它的五元组分别是如下含义:
Σ
是trie树上的边上的字符的集合。
δ
转移函数表示如下一个映射,当前状态为
p
,自动机接收了一个字符
q0
初始状态为空,即trie树的根。
F
是结束状态集合,也即trie树上表示某一个串在此结束的那些点。
显然,由这些定义,我们就可以完成多模板串匹配,先将所有模板建成trie树,再建立上述AC自动机,对于每一个输入的串st,我们从trie树的根开始,然后不断按照转移函数
那么如何构建AC自动机呢?
首先我们需要构建trie树,这样我们就构建好了除了转移函数以外的所有部分,那么剩下的我们就是要构建转移函数。
根据转移函数的定义,我们可以发现,假如当前状态是p,我们一直沿着p的
fail
一直走(显然
fail
与
ch
无关,这是由
fail
的定义可以知道的),可以得到一条
fail
链。那么对于我当前一个字符ch,假如我一直沿着
fail
链往前条到某一个状态
np
才有trie边
ch
指向
nq
,那么
p
到
对于目前状态
p
的转移函数
如果
p
存在trie边
否则,如果p是初始状态,则返回p,否则返回
delta(fail,ch)
;
那么又有一个问题,
fail
如何计算。
显然,由
fail
的定义我们知道,对于当前一个状态
p
,假如它有一个trie边
特别地,初始状态的
fail
还是初始状态,并且一切接受了一个字符的状态(即trie树中根的儿子)的
fail
都为根。
这样我们就完成了AC自动机的构建。
代码如下:
inline void build(){ int l, r;
rt->fail = rt;
for(q[l = r = 0] = (int)rt; l <= r; l++){
Node *p = (Node *)q[l];
REP(i, 26)
if (p->nxt[i]) q[++r] = (int)(p->nxt[i]), p->nxt[i]->fail = p == rt ? rt : p->fail->nxt[i];
else p->nxt[i] = p == rt ? rt : p->fail->nxt[i];
}
}
之后,我们发现AC自动机提供了一些副产品,比如fail树。
所谓fail树,就是把trie树中所有节点之间原来的边删去,仅保留fail边,然后反向所有fail边构成的树。
显然对于某个结点u,它的所有祖先都是它的后缀。
这样利用fail树我们可以知道对于给出的某一本字典,其中的任意一个单词在字典中出现了几次。