Aho Corasick Automata - AC自动机


问题

在文本中查找个模式出现的所有位置。其中长度为的长度为,其中最长的模式长度为,所有模式长度之和为,且所有模式两两互不重复。

简单字符串匹配算法

将简单字符串匹配SimpleMatch应用在本问题上,搜索所有模式需要重复次,每次的时间复杂度为。直接应用SimpleMatch算法的时间复杂度为

AhoCorasick1.png

前缀树

能否在一次匹配的过程中就同时找出所有模式呢?即并行算法(算法的并行,而非多线程/多进程的并行)。

首先用所有构造一个前缀树,如图所示:

AhoCorasick2.png

的首个字符开始,将其与前缀树中的节点依次向下匹配,可知,但因此匹配失败;

AhoCorasick3.png

匹配位置向右移动一位,从开始,可知

AhoCorasick4.png

匹配位置向右移动一位,从开始匹配,可知匹配成功;

构建前缀树的时间复杂度为。利用前缀树,文本上的每个字符匹配前缀树即可,该算法的时间复杂度为。当远大于时显然构造第二种算法更优。

失败指针

下图中,当匹配到时有,但匹配失败。我们不希望从处从前缀树的根节点重新开始匹配,显然在前缀树中已经存在。因为的后缀字符串,这时将前缀树的匹配位置调整到,那么可以继续匹配,尝试找到一个成功的匹配。图中红色的连线称为失败链接/失败指针

AhoCorasick5.png

设字符串的末尾字符为,尝试在前缀树中寻找的最长后缀字符串(设的末尾字符)。若找到这样一个合适的,建立从的指针,否则建立从的指针。显然前缀树中每个节点只有一个失败指针。失败指针的出发节点是前缀树中最后一个成功匹配的字符,其实质是后缀字符串,也称后缀链接/后缀指针

失败指针的核心思路在于匹配文本失败时,希望避免从前缀树的根部重新开始匹配。失败指针要么指向一个与当前位置上字符串相同的最长的后缀字符串(这样的指针就是后缀指针),要么指向前缀树的根节点。比如下图中的最长后缀字符串是的最长后缀字符串是找不到最长后缀字符串(也可以认为最长后缀字符串为空),因此有失败指针

AhoCorasick6.png

前缀树中的失败指针联系的两个节点可能在同一个字符串上,比如下图中有失败指针,这两个失败指针在前缀树中构成了环形图。

AhoCorasick7.png

输出指针

下图中,当匹配到时(即使在该位置没有匹配成功也同样适用),我们发现不管前缀树当前位置匹配成功与否,一定存在成功的匹配。利用这一特性避免了从和前缀树的根节点重新开始匹配。显然这也是失败指针,但并非在匹配失败时才跳转,这类跳转称为输出指针/输出链接,用红色虚线表示。

AhoCorasick8.png

再给一个特别情况,当匹配到时,有失败指针,输出指针。因此对于前缀树中的节点,需要递归的沿着所有失败指针,找出一次成功匹配。当匹配到时,有输出指针。如图所示:

AhoCorasick9.png

仔细观察可以发现,输出指针有几个特性:

两个节点不在同一个字符串分支上,是前缀树中的任意节点;

输出指针是一种特殊的失败指针,。显然每个节点上只有至多一个输出指针;

是前缀树中的叶子节点;

在匹配过程中,尝试递归的沿着前缀树上当前节点的失败指针,找出所有输出指针,这些输出指针都是(在其他分支的字符串上的)成功匹配。

最终得到AC自动机算法:对于文本上的任意字符,从前缀树的根部开始匹配:

沿着前缀树完成一次成功匹配,上的位置向右移动一位,从前缀树的根节点重新开始匹配;

匹配失败时,若前缀树上的当前节点上有非(非前缀树根节点)的失败指针,则跳到失败指针处继续匹配;若没有这样的失败指针,则文本的匹配位置向右移动一位,从前缀树的根节点重新开始匹配;

匹配途中若遇到输出指针,立刻找到一次输出指针所处的成功匹配,但不影响当前字符串分支上的匹配,当前的匹配仍然继续;

AC自动机的匹配时间复杂度为。其中是所有模式在文本上出现的次数。

构建AC自动机

构建AC自动机需要三步:构建前缀树;构建失败指针;构建输出指针。

构造前缀树的过程详见本书的DataStructure-PrefixTree。

构造失败指针的过程是一种类似BFS/层序遍历树的算法。初始时令根节点的失败指针指向自己,首先将前缀树的第一层节点加入空队列中,所有的失败指针指向根节点。然后依次从中取出头节点,对于头节点的某个孩子节点,寻找它的失败指针,并将推入中,直到为空:

对于前缀树根节点,其失败指针指向自己;

对于前缀树第一层节点,其失败指针指向

对于前缀树中其他的节点,设该节点的字符为,其父节点为,且的失败指针。若有字符为的孩子节点,则显然所在的字符串为所在字符串的最长后缀字符串。因此有失败指针。若不存在这样的孩子节点,则递归的再考虑的失败指针,直到失败指针本身是,则有失败指针,递归结束;如图所示:

AhoCorasick10.png

在构造失败指针的同时构造输出指针,若的失败指针不是前缀树根节点,又是前缀树的叶子节点,则有输出指针。显然不存在输出指针,前缀树第一层节点也都不存在输出指针。

AC自动机的构造时间复杂度为,加上匹配的时间,AC自动机算法的时间复杂度为


Aho Corasick Automata


源码

AhoCorasickAutomata.h

AhoCorasickAutomata.cpp

测试

AhoCorasickAutomataTest.cpp