前言
DAT每次转移的时间复杂度都是常数,全切分长度为n的文本时,时间复杂度是0(n2 ^22)
例子:
假设词典收录了所以的阿拉伯数字,那么对文本“123”进行扫描,发生了6次的状态转移
1、12、123;2、23;3
推广一下:“123···n”扫描就发生了n+(n-1)+(n-2)+···+1=2(n+1)n
=0(n2 ^22)次状态转移
但是,我们学习自然语言处理就为了更快,更牛逼!
AC自动机就是一种可以一次扫描查询出所有出现的单词的复杂度为0(n)的多模式匹配算法。
简单说一下AC自动机的AC,就是这俩人,贝尔实验室的Aho和Corasick。
提示:以下是本篇文章正文内容,下面案例可供参考
一、从字典树到AC自动机
前面说了例子,就是为了让一次扫描查询所有出现过的单词。我们过渡到了AC自动机。字典树是前缀树,AC自动机是在前缀树的基础上,为前缀树上的每个节点建立一颗后缀树。节省大量的查询时间。
AC自动机
由goto表
、fail表
和output表
组成。
1. goto表
goto表也称作success表,如下举例理解:
使用ushers为母文本,模式串集合为{he,she,his,hers}
此时的goto表为
它的构建和前缀树一致,唯一不同的地方是:根节点不光可以按照h和s转移,还接受其他任意字符,转移终点都是自己。这样形成了一个圈,圈的目的在于扫描时若遇到非h且非s的字符,状态机一直保持初始状态。
2.output表
这个很简单,写出对应的状态编号的输出值的表就行了
将其与goto表结合如下
output 表中的元素有两种,一种是从初始状态到当前状态的路径本身对应的模式串(比如2号状态),另一种是路径的后缀所对应的模式串(比如5号状态)。于是它的构造也分为两步,第一步与字典树类似,就是记录完整路径对应的模式串。第二步则是找出所有路径后缀及其模式串,这一步可以与fail表的构造同步进行。
3.fail表
fail表中保存了失配时的fail指针,fail指针就是当前位置失配以后能够跳转继续进行匹配的字符位置,达到匹配过程中不需要回溯的效果。构建过程使用了BFS(宽度优先搜索)算法。
晗佬的解释是:fail表保存的是状态间一对一的关系,存储状态转移失败后应当回到的最佳状态。最佳状态指的是能记住已匹配上的字符串的最长后缀的那个状态
仍然从实例来理解fail表如何创建
(1)初始状态的goto表是满的,永远不会失败,因此没有fail指针。与初始状态直接相连的所有状态,其fail指针都指向初始状态,如图2-11中的虚线所示。
(2)从初始状态开始进行广度优先遍历(BFS),若当前状态S接受子符c直达的状态为T,则沿着S的fail指针回溯,直到找到第一个前驱状态F,使得 F.goto( c ) != null。将T的fail指针设为F.goto( c ),也即:
F = S.fail while F.goto( c ) == null F = F.fail T.fail = F.goto( c )
(3)由于F路径是T路径的后缀,也就是说T一定包含F,因而T的 output也应包含F的output。于是更新:
T.output += F.output
完善后得出:(反复多对照图和上述流程理解就可)
从后往前看,便是后缀树,举例一个如下:
字典树状态转移可能失败,失败时扫描起点往右挪―下,重新扫描。而在AC自动机中,按goto表转移失败时就按fail表转移永远不会失败,因此只需扫描一遍文本。
二、代码实现(看看即可)
自动机状态部分代码
fail表建立部分代码:
演示上述经典案例ushers
三、速度测评
仍然是直接上晗佬的测评结果
可以看出,已经比BinTrie快了一点了,但比前面所需的DAT双数组字典树,还是逊色了一些,这里,我们就可以考虑,如果将双数组与AC自动机结合会怎么样呢??也就是下节的内容了。
总结
晗佬的书循序渐进,亲情推荐大家买来阅读,一步一步,相信我们都可以真正的去入门自然语言处理!