[AC自动机]个人对于AC自动机的理解

首先,AC自动机应该是一个FA,即有限状态自动机。
任意一个有限状态自动机 M 都是一个五元组。
M= { Q,Σ,δ,q0,F }
Q 是有限大小的状态集合,
Σ 是字符集,
δ 是转移函数,是一个状态集合到状态集合自身的对应关系。
q0 是初始状态,
F 是结束状态集合。

那么对于AC自动机而言,它是以trie树为基础,并以trie树结点为它的状态的自动机,它的五元组分别是如下含义:
Q 是有限大小的状态集合,当自动机处于当前状态 q 时,表示trie树的根到状态 q 的路径上的字符与目前自动机已经接收的字符串的后缀相同,并且这个后缀尽可能长(但不与原串等长)。
Σ 是trie树上的边上的字符的集合。
δ 转移函数表示如下一个映射,当前状态为 p ,自动机接收了一个字符 ch(chΣ) ,使自动机转移到另一个状态 q 。状态q满足,若 p 原本在trie树上就有一个儿子 q ,它们之间的连边为 ch ,那么转移函数就转移到q,如果不存在,则先找到一个状态 fail fail 是这样一个状态:trie树的根到 fail 上的字符组成的串 是 trie树的根到 p 上的字符组成的串 的一个后缀,并且这个后缀尽可能长,如果 fail 在trie树上有一条 ch 边指向儿子 q ,转移函数就转移到q,否则我们继续找fail的fail,直到fail有一条 ch 边位置。
q0 初始状态为空,即trie树的根。
F 是结束状态集合,也即trie树上表示某一个串在此结束的那些点。

显然,由这些定义,我们就可以完成多模板串匹配,先将所有模板建成trie树,再建立上述AC自动机,对于每一个输入的串st,我们从trie树的根开始,然后不断按照转移函数 δ 走,每当我们走到一个结束状态时,就意味着我们找到了一个模板串。

那么如何构建AC自动机呢?
首先我们需要构建trie树,这样我们就构建好了除了转移函数以外的所有部分,那么剩下的我们就是要构建转移函数。
根据转移函数的定义,我们可以发现,假如当前状态是p,我们一直沿着p的 fail 一直走(显然 fail ch 无关,这是由 fail 的定义可以知道的),可以得到一条 fail 链。那么对于我当前一个字符ch,假如我一直沿着 fail 链往前条到某一个状态 np 才有trie边 ch 指向 nq ,那么 p fp 之间的所有点的转移函数对于字符 ch 都应转移到 nq ,因此我们可以递归地来计算转移函数,即:
对于目前状态 p 的转移函数 δ(p,ch)
如果 p 存在trie边 ch 指向 q ,转移函数返回 q
否则,如果p是初始状态,则返回p,否则返回 delta(fail,ch)
那么又有一个问题, fail 如何计算。
显然,由 fail 的定义我们知道,对于当前一个状态 p ,假如它有一个trie边 ch 指向 q ,那么 q>fail=p>fail>delta(p>fail,ch)
特别地,初始状态的 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树我们可以知道对于给出的某一本字典,其中的任意一个单词在字典中出现了几次。

你可能感兴趣的:([AC自动机]个人对于AC自动机的理解)