本文为帮助初学者快速了解什么是AC自动机。
简介
要学习AC自动机,了解KMP算法是它的前提。KMP算法是单模式串的匹配,常见的KMP算法解决的问题类似为:在字符串ABCABCABD中查找模式串ABCABD,它可以优化查找的时间复杂度到O(n)。而AC自动机比KMP厉害点,处理的是多模式串的匹配,也就是可以在一个字符串中同时查找多个模式串。与KMP算法的思想核心一致,AC自动机处理问题的策略也是在匹配失败后,调转到一个合适的位置继续实现匹配。在KMP中,我们使用next[i]数组来实现在模式串任意位置匹配失败时,调转到下一个位置继续匹配,而不会重复某些不必要的匹配过程。在AC自动机中,我们用fail指针来实现同样的功能,当匹配失败时,沿着fail指针调转到另一个状态继续匹配而不会有冗余的过程。
具体来看看AC自动机长什么样的。AC自动机由字典树(Trie)和fail指针构成。比如说我们有题目:在字符串amorebmonebugly中,统计more、on、ugly、bug这四个单词各出现了多少次。
构造字典树Trim
首先,我们把四个模式串构造成一棵字典树(Trim),我们需要设立一个空的root节点,从这点出发开始构造。构造成下图:
树中每一个节点都表示一个状态,从root节点出发,读入字符串,每读入一个字符,且能够与当前节点的某个孩子匹配,就向下走一步到匹配的孩子节点处。当走到图中蓝色标记的状态时,就完成识别一次字符串的匹配过程。
上述过程很简单,沿着Trim往下走就是了,问题在于,如果读入一个字符,没有一个孩子匹配,不能继续往下走时应该怎么办?这时就要用到构造AC自动机的第二个要素,构造fail指针。
构造fail指针
fail指针实现的是,当读入一个字符,匹配失败时,应该跳转到哪一个状态下继续匹配。先来感性的体会一下fail指针的作用:
比如我们读入了‘m'、'o'两个字符,这时在Trim上走的路径是root->m->o,待读入'n'字符,但是’o‘的下一个状态是'r',匹配失败,但是已经读入的字符'o'和root->o这条路径匹配,所以应该跳转到root->o,从'o'继续向下匹配,发现读入'n'后匹配成功。
这就是fail指针的功能。接下来我们来解决如何构造fail指针。
- root节点的fail指向自己,root节点下一层的fail指针都指向root节点。
- 从第二层开始,每个节点X沿着父节点的fail指针到达一个节点Y,如果节点Y的孩子中同样也有节点X,那么节点X的fail指针指向节点Y下的节点X。
- 如果不存在同样的节点X,则继续沿着当前沿着找到的节点Y的fail指针向上找,做同样的判断。如果到root点时任未找到,则节点X的fail指针指向root节点。
整个构造过程可以用广度搜索从上到下、从左到右的实现。构造好的fail指针后的图像如下想所示,红色线条表示各节点fail指针的指向。
根据AC自动机的转态转移,对于文中所提问题,有下面程序转态转移流程。上一行是字符串的输入,下一行是在AC自动机中状态的转移情况。其中绿色表示识别到一个字符,红色表示匹配失败,沿fail指针转移到某个状态。