强烈推荐这个网址:http://blog.ivank.net/aho-corasick-algorithm-in-as3.html ,提供了该算法的自主动态演示
书籍推荐:《柔性字符串匹配》
AC自动机是著名的贝尔实验室1975年搞出来的东西,我很想知道这么多年过去了有没有新的多模匹配算法搞出来,查了一下百度,还真有不少,这篇文章中介绍了一些 https://wenku.baidu.com/view/8924ed1b6bd97f192279e92b.html ,不过也只是截止到9几年了,有没有大佬给我一个最新的好用算法,这样子万一要搞什么AC自动机优化防TLE,MLE我也不怕了
言归正题:
Aho-Corasick这个看起来就懵逼的东西到底能干什么?
这家伙是一个经典的多模匹配算法,若给定一个字符串s,和若干字符串s1,s2,....要求出s中所包含的s1, s2.....所有字符串的次数总和,需要复杂度为O(n+m),其中n为匹配串s的长度, m为s1,s2,.....模式串的长度和
如何实现Aho-Corasick?就简单的来说分三步:将多个patterns构成Trie树,构造失配指针,模式串匹配 (好你现在已经可以去A题了) Aho-Corasick = Trie + KMp
以常见的“ushers“为例,模式串为he / she/ hers/ his, 构造的AC自动机效果图:(建议按照所给演示网址直观感受一下)
状态机优先按照实线标注的状态转换路径进行转换,当所有实线标注的状态转换路径条件不能满足时,按照虚线的状态转换路径进行状态转换。如:状态0时,当输入h,则转换到状态1;输入s,则转换到状态3;否则转换到状态0。
让我们来定义几个东西:
① 状态跳转表goto: 决定对于当前状态S和条件C,如何得到下一状态S'
② 失配跳转表fail:决定goto表得到的下一状态无效时,应该退回到哪一状态
③ 匹配结果输出表output: 决定在哪个状态输出哪个恰好匹配的模式
来具体解释一下:
①goto表:这家伙实际上就是一棵Trie树,只看上图中的实现部分你就明白了,g(pre, x)=next:状态pre在输入一个字符x后转换为状态next。如果在模式串中不存在这样的转换,则next=failstate。
②fail表:f(per) = next,是在比较失配的情况下使用的转换关系,发生失配后,我们的字符串该前往哪里呢?这里我们不妨使用一下KMP算法的思想,跳转到的failstate, 我们从这个状态点回溯到根节点的所经历的输入字符与产生fail结点回溯的经历的完全相同,并且这个状态点,是所有满足上述条件中深度最大的那个,若不存在,则失配函数为0
在构造failure表时,我们会用到递归的思想:
1、若depth(s) = 1,则f(s) = 0;即与状态0距离为1(即深度为1)的所有状态的fail值都为0
2、假设深度为d-1的所有状态r, 即depth(r) < d,已经计算出了f(r);
3、那么对于深度为d的状态s:
(1) 若所有的字符a, 满足g(r,a) = fail,则不动作;(注:g为状态转移函数);
(2) 否则,对每个使 g(r, a) = s成立的字符a,执行以下操作::
a、使state = f(r)
b、重复步骤state = f(state), 直到g(state, a) != fail.(注意对于任意的a, 状态0的g(0,a) != fail)
c、使f(s) = g(state, a)。
③ouput表:output(i)={P},表示当状态机到达状态i时,模式串集合{P}中的所有模式串可能已经完成匹配
例:以上述数据所示
转向函数:
失效函数:
输出函数:
整体实现流程图:
知道了Aho-Corasick这东西的大体实现机理和结构,接下来就是AC代码实现了
先来一个预处理流程:
具体实现如下:
1.模式匹配实现
Algorithm 1: Pattern matching machine
Input: A text string x = a1a2...an wher each ai is an input symbol
ans a pattern matching machine M with goto function g, failure
function f, and ouput, as described above
Output: Locations at which keywords occur in x.
Method:
begin
state <- 0
for i <- 1 until n do //吞入text中的ai
begin
while g(state, ai) = fail do state <- f(state) //直到能走下去
state <- g(state, ai) //得到下一个状态
if output(state) ≠ empty then //能输出就输出
begin
print i
print output(state)
end
end
end
2.goto表构造
Algorithm 2: Construction of the goto function
Input : patterns_set K = {y1, y2, .., yk}
Output: goto function g and some of output function ouput
/*
We assume output(s) is empty when state s is first created, and g(s,a)=fail if a is
undefined or if g(s,a) has not yet been defined. The procedure enter(y) inserts into
the goto graph a path that spells out y.
*/
Method:
begin
newstate <- 0
for i <- 1 until k do enter(yi)
for all a such that g(0, a = fail) do g(0, a) <- 0
end
procedure enter(a1 a2...am)
begin
state <- 0; j <- 1
while g(state, aj) ≠ fail do
begin
state <- g(state, aj)
j <- j+1
end
for p <- j until m do //拓展新路径
begin
newstate <- newstate + 1
g(state, ap) <- newstate
state <- newstate
end
output(state) <- {a1 a2...am} //此处state为每次构造完一个pat时遇到的状态
end
3. failure表构造:依据上文中的failure递归思想,我们来看下实例(还是以上文数据为基准)
首先我们将depth为1的状态f(1)=f(3)=0,然后考虑depth为2的结点2,6,4
计算f(2)时候,我们设置state=f(1)=0,因为g(0,e)=0,所以f(2)=0;
计算f(6)时候,我们设置state=f(1)=0,因为g(0,i)=0,所以f(6)=0;
计算f(4)时候,我们设置state=f(3)=0,因为g(0,h)=1,所以f(4)=1;
然后考虑depth为3的结点8,7,5
计算f(8)时候,我们设置state=f(2)=0,因为g(0,r)=0,所以f(8)=0;
计算f(7)时候,我们设置state=f(6)=0,因为g(0,s)=3,所以f(7)=3;
计算f(5)时候,我们设置state=f(4)=1,因为g(1,e)=2,所以f(5)=2;
最后考虑depth为4的结点9
计算f(9)时候,我们设置state=f(8)=0,因为g(0,s)=3,所以f(9)=3;
Algorithm 3: Construction of the failure function
Input: goto function g and output function output from Algorithm 2
Output: failure function f and output function output
Method:
begin
queue <- empty
for each a such that g(0, a) = s ≠ 0 do // BFS
begin
queue <- queue ∪ {s}
f(s) <- 0
end
while queue ≠ empty do
begin
let r be the next state in queue
queue <- queue <- {r}
for each a such that g(r,a) = s ≠ fail do //r是队列头状态
begin
queue <- queue ∪ {s}
state <- f(r) // 与r同前缀的state
while g(state, a) = fail do state <- f(state)
f(s) <- g(state, a) //这一步相当于找到了s的同前缀状态
output(s) <- output(s) ∪ output(f(s))
end
end
end
基本实现的伪代码就到这里了,但是在ACM中,我们常常会要对传统代码进行优化,对于Aho-Corasick的优化有很多,比较高端的是Double trie_Aho-Corasick, 这里说两个低端的:
改进1:观察一下算法3中的failure function还不够优化
我们可以看到g(4,e)=5,如果现在状态到了4并且当前的字符为t!=e,因为g(4,t)=fail,
所以就根据f(4)=1,跳转到状态1,而之前已经知道t!=e,所以就没必要跳到1,而直接跳到状态f(1)=0。
为了避免不必要的状态迁移,和KMP算法有异曲同工之处。重新定义了一个failure function:f1
f1(1)=0, 对于i>1,如果能使状态f(i)转移的所有字符也能使i状态转移,那么f1(i)=f1(f(i)), 否则f1(i)=f(i)。 |
改进2:由于goto function中并不是每个状态对应任何一个字符都有状态迁移的,当迁移为fail的时候,我们就要查failure function,然后换个状态迁移。现在我们根据goto function和failure function来构造一个确定的有限自动机next move function,该自动机的每个状态遇到每个字符都可以进行状态迁移,这样就省略了failure function。
构造next move function的算法如下:
算法4:Construction of a deterministic finite automaton 输入:goto functioni g and failure function f 输出:next move function delta
queue←empty for each symbol a delta(0,a)←g(0,a) if g(0,a)≠0 queue←queue∪g(0,a)
while queue≠empty pop() for each symbol a if g(r,a)=s≠fail queue←queue∪{s} delta(r,a)←s else delta(r,a)←delta(f(r),a) |
Next function delta的计算如下:
其中’.’表示除了该状态能识别字符的其他字符。
最后就是大家最想要的了,真正的AC代码实现:
不给,嘿嘿嘿
我的另一篇博客中写了,大家去找吧
===================华丽的分割线================
没有VPN,无法上Google真的是心碎了,关于Aho-Corasick算法的资料其实一直找的不是很全,我这篇文章也是讲很多篇文章整合了一下,在此对那些博主表示深深的感激,同时因为篇幅问题(好吧,是我懒,不想打这么多字),有许多内容未能详细讲解。我是想找到贝尔实验室1975年的那篇关于Aho-Corasick的论文的,奈何能耐有限,只得从他人的文章中零零散散的找出来一些,若有人能给我那份论文(or《柔性字符串匹配》扫描版)本人将不胜感激。