1. 简介
AC自动机是一种多模匹配的文本匹配算法。
如果采用naive的方法,即依次比较文本串s中是否包含模式串p1, p2,...非常耗时。考虑到这些模式串中可能具有相同子串,可以利用已经比较过的那些模式串的一些信息,来优化效率。容易想到的一种方法是为这些模式串构建一个trie树,可以较好的利用模式串的公共前缀信息。
但是如果只是采用普通的trie树,仍有 如果一个模式串P1不匹配,就要重新回到根节点再找下一个模式串P2,也就是对于下一个模式串P2,要从P2的起始元素开始,依次与文本串S比较。这同样不够高效。P2如果可以利用和P1的一些共性信息,使得可以从P2的尽可能靠后的元素开始,与文本串S比较,那么算法的时间复杂度可能会有效降低。
AC自动机采用了KMP算法找next的思路,为trie树中每个节点找fail节点。
KMP中next与AC中fail的区别:
KMP算法 中 next[j] = k 表示前缀 [0 ~ (k - 1)] 与 后缀 [(j - k) ~ (j - 1)] 这k个元素是对应相等的,但是P[j] 和 P[k] 是不一定相等要在下一次进行比较的;
AC自动机中则是 cur = cur.fail,也即从 [根节点 ~ cur.fail (包含cur.fail节点)] 这个前缀,与 [cur节点所在路径上某节点 ~ cur节点(包含cur节点)] 这个后缀,对应相等。
2. 算法原理
(转自参考链接1)
2.1 初识AC自动机
AC自动机的基础是Trie树。和Trie树不同的是,树中的每个结点除了有指向孩子的指针(或者说引用),还有一个fail指针,它表示输入的字符与当前结点的所有孩子结点都不匹配时(注意,不是和该结点本身不匹配),自动机的状态应转移到的状态(或者说应该转移到的结点)。fail指针的功能可以类比于KMP算法中next数组的功能。
我们现在来看一个用目标字符串集合{abd,abdk, abchijn, chnit, ijabdf, ijaij}构造出来的AC自动机
上图是一个构建好的AC自动机,其中根结点不存储任何字符,根结点的fail指针为null。虚线表示该结点的fail指针的指向,所有表示字符串的最后一个字符的结点外部都用红圈表示,我们称该结点为这个字符串的终结结点。每个结点实际上都有fail指针,但为了表示方便,本文约定一个原则,即所有指向根结点的 fail虚线都未画出。
从上图中的AC自动机,我们可以看出一个重要的性质:每个结点的fail指针表示由根结点到该结点所组成的字符序列的所有后缀 和 整个目标字符串集合(也就是整个Trie树)中的所有前缀 两者中最长公共的部分。
比如图中,由根结点到目标字符串“ijabdf”中的 ‘d’组成的字符序列“ijabd”的所有后缀在整个目标字符串集{abd,abdk, abchijn, chnit, ijabdf, ijaij}的所有前缀中最长公共的部分就是abd,而图中d结点(字符串“ijabdf”中的这个d)的fail正是指向了字符序列abd的最后一个字符。
2.2 AC自动机的运行过程:
1)表示当前结点的指针指向AC自动机的根结点,即curr = root
2)从文本串中读取(下)一个字符
3)从当前结点的所有孩子结点中寻找与该字符匹配的结点,
若成功:判断当前结点以及当前结点fail指向的结点是否表示一个字符串的结束,若是,则将文本串中索引起点记录在对应字符串保存结果集合中(索引起点= 当前索引-字符串长度+1)。curr指向该孩子结点,继续执行第2步
若失败:执行第4步。
4)若fail == null(说明目标字符串中没有任何字符串是输入字符串的前缀,相当于重启状态机)curr = root, 执行步骤2,
否则,将当前结点的指针指向fail结点,执行步骤3)
2.3 例子
来一个具体的例子加深理解,初始时当前结点为root结点,我们现在假设文本串text = “abchnijabdfk”。
图中的紫色实曲线表示了整个搜索过程中的当前结点指针的转移过程,结点旁的文字表示了当前结点下读取的文本串字符。比如初始时,当前指针指向根结点时,输入字符‘a’,则当前指针指向结点a,此时再输入字符‘b’,自动机状态转移到结点b,……,以此类推。图中AC自动机的最后状态只是恰好回到根结点,并不一定都会回到根节点。
需要说明的是,当指针位于结点b(图中曲线经过了两次b,这里指第二次的b,即目标字符串“ijabdf”中的b),这时读取文本串字符下标为9的字符(即‘d’)时,由于b的所有孩子结点(这里恰好只有一个孩子结点)中存在能够匹配输入字符d的结点,那么当前结点指针就指向了结点d,而此时该结点d的fail指针指向的结点又恰好表示了字符串“abc”的终结结点(用红圈表示),所以我们找到了目标字符串“abc”一次。这个过程我们在图中用虚线表示,但状态没有转移到“abd”中的d结点。
在输入完所有文本串字符后,我们在文本串中找到了目标字符串集合中的abd一次,位于文本串中下标为7的位置;目标字符串ijabdf一次,位于文本串中下标为5的位置。
3. 构造AC自动机的方法与原理
首先我们将所有的目标字符串插入到Trie树中,然后通过广度优先遍历为每个结点的所有孩子节点的fail指针找到正确的指向。
确定fail指针指向的问题和KMP算法中构造next数组的方式如出一辙。具体方法如下
1)将根结点的所有孩子结点的fail指向根结点,然后将根结点的所有孩子结点依次入列。
2)若队列不为空:
2.1)出列,我们将出列的结点记为curr, failTo表示curr的fail指向的结点,即failTo = curr.fail
2.2) a.判断curr.child[i] == failTo.child[i]是否成立,
成立:curr.child[i].fail = failTo.child[i],
不成立:判断 failTo == null是否成立
成立: curr.child[i].fail == root
不成立:执行failTo = failTo.fail,继续执行2.2)
b.curr.child[i]入列,再次执行再次执行步骤2)
若队列为空:结束
4. 代码实现
1 #coding:utf-8 2 import queue 3 4 class Node(object): 5 def __init__(self): 6 self.children = {} 7 self.fail = None 8 self.isWord = False 9 self.word = "" 10 11 class ACAutomation(object): 12 """ AC Automation 13 """ 14 def __init__(self): 15 self.root = Node() 16 17 def add(self, word): 18 cur_node = self.root 19 for char in word: 20 if char not in cur_node.children: 21 cur_node.children[char] = Node() 22 cur_node = cur_node.children[char] 23 cur_node.isWord = True 24 cur_node.word = word 25 26 def link_fail(self): 27 que = queue.Queue() 28 que.put(self.root) 29 30 while que.empty() == False: 31 32 cur_node = que.get() 33 cur_fail = cur_node.fail 34 35 for child_key, child_value in cur_node.children.items(): 36 37 while True: 38 if cur_fail is None: 39 cur_node.children[child_key].fail = self.root 40 break 41 42 elif child_key in cur_fail.children: 43 cur_node.children[child_key].fail = cur_fail.children[child_key] 44 break 45 46 else: 47 cur_fail = cur_fail.fail 48 49 que.put(cur_node.children[child_key]) 50 51 52 def curWords(self, cur_node): 53 """ 该函数为查找当前节点处所有可能的匹配的模式串的集合 54 Args: 55 cur_node 当前节点 56 Returns: 57 set 当前节点处所有可能的匹配的模式串的集合 58 59 匹配成功模式串有两种情况: 60 1. 当前节点处 isWord = True, 则匹配的模式串即为 cur_node.word (如图例'ijabdf') 61 2. 当前节点的fail节点处 isWord = True, 则匹配的模式串为 cur_node.fail.word 62 (如图例 'abd',文字标红处有解释) 63 (当然fail节点也可能有fail.fail...需要while循环继续推一下.) 64 """ 65 ret = set() 66 cur_fail = cur_node.fail 67 if cur_node.isWord: 68 ret.add(cur_node.word) 69 #ret.append(cur_node.word) 70 while cur_fail is not None and cur_fail is not self.root: 71 if cur_fail.isWord: 72 #ret.append(cur_fail.word) 73 ret.add(cur_fail.word) 74 cur_fail = cur_fail.fail 75 return ret 76 77 def search(self, s): 78 cur_node = self.root 79 # result = [] 80 result = set() 81 cur_pos = 0 82 83 while cur_pos < len(seq): 84 word = seq[cur_pos] 85 #result.extend(self.curWords(cur_node)) 86 87 while word in cur_node.children == False and cur_node != self.root: 88 # result.extend(self.curWords(cur_node)) 89 result |= self.curWords(cur_node) 90 cur_node = cur_node.fail 91 92 if word in cur_node.children: 93 #result.extend(self.curWords(cur_node)) 94 result |= self.curWords(cur_node) 95 cur_node = cur_node.children[word] 96 97 else: 98 cur_node = self.root 99 100 result |= self.curWords(cur_node) 101 102 cur_pos += 1 103 104 return list(result) 105 106 ac = ACAutomation() 107 l = ['abd', 'abdk', 'abchijn', 'chnit', 'ijabdf', 'ijaij'] 108 for e in l: 109 ac.add(e) 110 ac.link_fail() 111 seq = 'abchnijabdfk' 112 ret = ac.search(seq) 113 print(ret) 114 115 """output 116 """ 117 # ['abd', 'ijabdf']
参考链接:
1. 多模字符串匹配算法之AC自动机—原理与实现:https://www.cnblogs.com/nullzx/p/7499397.html
2. 从头到尾彻底理解KMP:https://www.cnblogs.com/zhangtianq/p/5839909.html