Aho-Corasick懵逼学习

强烈推荐这个网址: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

wps_clip_image-1497

在构造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}中的所有模式串可能已经完成匹配

例:以上述数据所示

转向函数:

wps_clip_image-1758

失效函数:

wps_clip_image-1780

输出函数:

wps_clip_image-1801

整体实现流程图:

知道了Aho-Corasick这东西的大体实现机理和结构,接下来就是AC代码实现了

先来一个预处理流程:

wps_clip_image-2124

具体实现如下:

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递归思想,我们来看下实例(还是以上文数据为基准)

首先我们将depth1的状态f(1)=f(3)=0,然后考虑depth2的结点264

计算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;

然后考虑depth3的结点875

计算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;

最后考虑depth4的结点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还不够优化

 改进1

 

我们可以看到g(4,e)=5,如果现在状态到了4并且当前的字符为t=e,因为g(4,t)=fail

所以就根据f(4)=1,跳转到状态1,而之前已经知道t=e,所以就没必要跳到1,而直接跳到状态f(1)=0

为了避免不必要的状态迁移,和KMP算法有异曲同工之处。重新定义了一个failure functionf1

f1(1)=0

对于i>1,如果能使状态f(i)转移的所有字符也能使i状态转移,那么f1(i)=f1(f(i))

否则f1(i)=f(i)

改进2由于goto function中并不是每个状态对应任何一个字符都有状态迁移的,当迁移为fail的时候,我们就要查failure function,然后换个状态迁移。现在我们根据goto functionfailure function来构造一个确定的有限自动机next move function,该自动机的每个状态遇到每个字符都可以进行状态迁移,这样就省略了failure function

构造next move function的算法如下:

算法4Construction of a deterministic finite automaton

输入:goto functioni g and failure function f

输出:next move function delta

 

queueempty

for each symbol a

     delta(0,a)g(0,a)

     if g(0,a)0

         queuequeueg(0,a)

 

while queueempty

     pop()

     for each symbol a

         if g(r,a)=sfail

              queuequeue{s}

              delta(r,a)s

else delta(r,a)delta(f(r),a)

Next function delta的计算如下:

 改进2

其中’.’表示除了该状态能识别字符的其他字符。

最后就是大家最想要的了,真正的AC代码实现:

不给,嘿嘿嘿

我的另一篇博客中写了,大家去找吧

===================华丽的分割线================

没有VPN,无法上Google真的是心碎了,关于Aho-Corasick算法的资料其实一直找的不是很全,我这篇文章也是讲很多篇文章整合了一下,在此对那些博主表示深深的感激,同时因为篇幅问题(好吧,是我懒,不想打这么多字),有许多内容未能详细讲解。我是想找到贝尔实验室1975年的那篇关于Aho-Corasick的论文的,奈何能耐有限,只得从他人的文章中零零散散的找出来一些,若有人能给我那份论文(or《柔性字符串匹配》扫描版)本人将不胜感激。

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Aho-Corasick算法是一种高效的字符串匹配算法,它可以在一次扫描文本的过程中同时查找多个模式串。该算法通过将模式串预处理为确定有限状态自动机,从而实现快速的匹配过程。与其他算法相比,Aho-Corasick算法的时间复杂度为O(n),与模式串的数量和长度无关。 以下是Aho-Corasick算法的一种Java实现示例: ```java import java.util.*; class AhoCorasick { static class Node { Map<Character, Node> children; Node fail; List<String> outputs; Node() { children = new HashMap<>(); fail = null; outputs = new ArrayList<>(); } } static void buildTrie(Node root, List<String> patterns) { for (String pattern : patterns) { Node curr = root; for (char c : pattern.toCharArray()) { curr.children.putIfAbsent(c, new Node()); curr = curr.children.get(c); } curr.outputs.add(pattern); } } static void buildFailure(Node root) { Queue<Node> queue = new LinkedList<>(); for (Node child : root.children.values()) { child.fail = root; queue.add(child); } while (!queue.isEmpty()) { Node curr = queue.poll(); for (Map.Entry<Character, Node> entry : curr.children.entrySet()) { char c = entry.getKey(); Node child = entry.getValue(); queue.add(child); Node failNode = curr.fail; while (failNode != null && !failNode.children.containsKey(c)) { failNode = failNode.fail; } child.fail = failNode != null ? failNode.children.get(c) : root; child.outputs.addAll(child.fail.outputs); } } } static List<String> search(Node root, String text) { List<String> matches = new ArrayList<>(); Node curr = root; for (char c : text.toCharArray()) { while (curr != null && !curr.children.containsKey(c)) { curr = curr.fail; } curr = curr != null ? curr.children.get(c) : root; for (String output : curr.outputs) { matches.add(output); } } return matches; } public static void main(String[] args) { List<String> patterns = Arrays.asList("he", "she", "his", "hers"); String text = "ushers"; Node root = new Node(); buildTrie(root, patterns); buildFailure(root); List<String> matches = search(root, text); System.out.println(matches); // 输出:[she, he, hers] } } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值