[转]AC自动机算法详解




AC算法--多模式匹配(论文解析版)


       早在1975年贝尔实验室的两位研究人员Alfred V. Aho 和Margaret J. Corasick就提出了以他们的名字命名的高效的匹配算法---AC算法。该算法几乎与KMP算法http://blog.csdn.net/myjoying/article/details/7947119)同时问世。与KMP算法相同,AC算法时至今日仍然在模式匹配领域被广泛应用。

         最近本人由于兴趣所致,在学习完KMP和BM单模式匹配算法之后,开始学习多模式匹配算法。看了一些博文和中文文章之后发现学习AC算法的最有效方式仍然是Aho和Corasick的论文《Efficient String Matching: An Aid to Bibliographic Search》。本文算是对这篇文章的一些总结。

 

AC算法思想        

          多模式匹配AC算法的核心仍然是寻找模式串内部规律,达到在每次失配时的高效跳转。这一点与单模式匹配KMP算法和BM算法是一致的。不同的是,AC算法寻找的是模式串之间的相同前缀关系。AC算法的核心是三张查找表:goto、failure和output,共包含四种具体的算法,分别是计算三张查找表的算法以及AC算法本身。

 

构造goto表

        goto表本质上是一个有限状态机,这里称作模式匹配机(Pattern Matching Machine,PMM)。下面以论文中的例子来说明goto表的构造过程。对于模式串集合K{he, she, his, hers}

         第一步:PMM初始状态为0,然后向PMM中加入第一个模式串K[0] = "he"。

          

         第二步:继续向PMM中添加第二个模式串K[1] = "she",每次添加都是从状态0开始扫描。

             

          第三步:从状态0开始继续添加第三个模式串K[2] = "his",这里值得注意的是遇到相同字符跳转时要重复利用以前已经生成的跳转。如这里的'h'在第一步中已经存在。

         

          第四步:添加模式串K[3] = "hers"。至此,goto表已经构造完成。

            

 

构造failure表

          failure表作用是在goto表中匹配失败后状态跳转的依据,这点与KMP中next表的作用相似。首先引入状态深度的概念,状态s的深度depth(s)定义为在goto表中从起始状态0到状态s的最短路径长度。如goto表中状态1和3的深度为1。

         构造failure表利用到了递归的思想。

          1、若depth(s) = 1,则f(s) = 0;

          2、假设对于depth(r) < d的所有状态r,已近计算出了f(r);

          3、对于深度为d的状态s:

                (1) 若g(r,a) = fail,对于所有的a,则不动作;(注:a为字符,g为状态转移函数);

                (2) 否则,对于a使得g(r,a) = s,则如下步骤:

                      a、使state = f(r)

                      b、重复步骤state = f(state),直到g(state, a) != fail。(注意对于任意的a,状态0的g(0,a) != fail)

                      c、使f(s) = g(state, a)。

 

        根据以上算法,得到该例子的failure表为:

         i     1  2  3  4  5  6  7  8  9

       f(i)    0  0  0  1  2  0  3  0  3 

 

构造output表

       output表示输出,即代表到达某个状态后某个模式串匹配成功。该表的构造过程融合在goto表和failure表的构造过程中。

       1、在构造goto表时,每个模式串结束的状态都加入到output表中,得到

            i           output(i)

            2           {he}

            5           {she}

            7           {his}

            9           {hers}

       2、在构造failure表时,若f(s) = s',则将s和s‘对应的output集合求并集。如f(5) = 2,则得到最终的output表为:

            i           output(i)

            2           {he}

            5           {she,he}

            7           {his}

            9           {hers}

 

AC算法实现

         根据上面已经构造好的goto、failure和output表就可以方便地应用AC算法了。

         剩下的工作就是将目标串依次输入到PMM(即goto表),然后在发生失配的时候查找failure表实现跳转,在输出状态查找output表输出结果(包括匹配的串的集合和目标串中的位置)。以字符串"ushers"为例。状态转移如下:

          u      s     h     e      r      s

        0    0     3     4      5     8      9

                                    2

说明:在状态5发生失配,查找failure表,转到状态2继续比较。在状态5和状态9有输出。

 

算法改进

       值得注意的是在AC算法的以上实现中,对于输入字符a的跳转次数是不确定的。因为有可能在输入a后发生失配,需要查找failure表重新跳转。能不能在PMM的基础上实现确定型的有限状态机,即对于任意字符a,都只用进行一次确定的状态跳转?答案是肯定的。在Aho和Corasick论文中给出了处理的方法:构造一个与KMP算法中相似的next表,实现确定性跳转。

        next表的构造需要在goto表和failure表的基础上得到。next表如下所示:

 

                       输入字符                  下一状态       

         state 0:        h                              1

                             s                              3

                             *                               0

        state 1:         e                              2

                             i                               6

                             h                              1

                             s                               3

                             *                               0

     state9,7,3:       h                               4

                             s                               3

                             *                                0

        state5,2:       r                                8

                             h                               1

                             s                                3

                             *                                0 

           state 6:      s                                7

                             h                                1

                             *                                 0

     state 4:           e                                 5

                             i                                  6

                             h                                 1

                            s                                  3

                           *                                   0

     state 8:            s                                 9

                             h                                 1

                             *                                  0

注:*表示除了以上字符以外的其他字符。存储next数组所需要的内存空间比存储goto表更大。这样就可以用next表替代goto表和failure表,实现每次字符输入的唯一跳转。

 


来源:http://blog.csdn.net/myjoying/article/details/7960534


AC自动机算法详解


 

    首先简要介绍一下AC自动机:Aho-Corasick automation,该算法在1975年产生于贝尔实验室,是著名的多模匹配算法之一。一个常见的例子就是给出n个单词,再给出一段包含m个字符的文章,让你找出有多少个单词在文章里出现过。要搞懂AC自动机,先得有模式树(字典树)Trie和KMP模式匹配算法的基础知识。AC自动机算法分为3步:构造一棵Trie树,构造失败指针和模式匹配过程。
     如果你对KMP算法和了解的话,应该知道KMP算法中的next函数(shift函数或者fail函数)是干什么用的。KMP中我们用两个指针i和j分别表示,A[i-j+ 1..i]与B[1..j]完全相等。也就是说,i是不断增加的,随着i的增加j相应地变化,且j满足以A[i]结尾的长度为j的字符串正好匹配B串的前 j个字符,当A[i+1]≠B[j+1],KMP的策略是调整j的位置(减小j值)使得A[i-j+1..i]与B[1..j]保持匹配且新的B[j+1]恰好与A[i+1]匹配,而next函数恰恰记录了这个j应该调整到的位置。同样AC自动机的失败指针具有同样的功能,也就是说当我们的模式串在Tire上进行匹配时,如果与当前节点的关键字不能继续匹配的时候,就应该去当前节点的失败指针所指向的节点继续进行匹配。
      看下面这个例子:给定5个单词:say she shr he her,然后给定一个字符串yasherhs。问一共有多少单词在这个字符串中出现过。我们先规定一下AC自动机所需要的一些数据结构,方便接下去的编程。

 1  const   int  kind  =   26
 2  struct  node{  
 3      node  * fail;        // 失败指针
 4      node  * next[kind];  // Tire每个节点的个子节点(最多个字母)
 5       int  count;         // 是否为该单词的最后一个节点
 6      node(){            // 构造函数初始化
 7          fail = NULL; 
 8          count = 0
 9          memset(next,NULL, sizeof (next)); 
10      } 
11  } * q[ 500001 ];           // 队列,方便用于bfs构造失败指针
12  char  keyword[ 51 ];      // 输入的单词
13  char  str[ 1000001 ];     // 模式串
14  int  head,tail;         // 队列的头尾指针

 

 

有了这些数据结构之后,就可以开始编程了:
   
首先,将这5个单词构造成一棵Tire,如图-1所示。


 

 1  void  insert( char   * str,node  * root){ 
 2      node  * p = root; 
 3       int  i = 0 ,index;  
 4       while (str[i]){ 
 5          index = str[i] - ' a '
 6           if (p -> next[index] == NULL) p -> next[index] = new  node();  
 7          p = p -> next[index];
 8          i ++ ;
 9      } 
10      p -> count ++ ;      // 在单词的最后一个节点count+1,代表一个单词
11  }

 

在构造完这棵Tire之后,接下去的工作就是构造下失败指针。构造失败指针的过程概括起来就一句话:设这个节点上的字母为C,沿着他父亲的失败指针走,直到走到一个节点,他的儿子中也有字母为C的节点。然后把当前节点的失败指针指向那个字母也为C的儿子。如果一直走到了root都没找到,那就把失败指针指向root。具体操作起来只需要:先把root加入队列(root的失败指针指向自己或者NULL),这以后我们每处理一个点,就把它的所有儿子加入队列,队列为空。

 

 1  void  build_ac_automation(node  * root){
 2     int  i;
 3      root -> fail = NULL; 
 4      q[head ++ ] = root; 
 5       while (head != tail){ 
 6          node  * temp = q[tail ++ ]; 
 7          node  * p = NULL; 
 8           for (i = 0 ;i < 26 ;i ++ ){ 
 9               if (temp -> next[i] != NULL){ 
10                   if (temp == root) temp -> next[i] -> fail = root;                 
11                   else
12                      p = temp -> fail; 
13                       while (p != NULL){  
14                           if (p -> next[i] != NULL){ 
15                              temp -> next[i] -> fail = p -> next[i]; 
16                               break
17                          } 
18                          p = p -> fail; 
19                      } 
20                       if (p == NULL) temp -> next[i] -> fail = root; 
21                  } 
22                  q[head ++ ] = temp -> next[i];  
23              } 
24          }   
25      } 
26  }


    从代码观察下构造失败指针的流程:对照图-2来看,首先root的fail指针指向NULL,然后root入队,进入循环。第1次循环的时候,我们需要处理2个节点:root->next[‘h’-‘a’](节点h) 和 root->next[‘s’-‘a’](节点s)。把这2个节点的失败指针指向root,并且先后进入队列,失败指针的指向对应图-2中的(1),(2)两条虚线;第2次进入循环后,从队列中先弹出h,接下来p指向h节点的fail指针指向的节点,也就是root;进入第13行的循环后,p=p->fail也就是p=NULL,这时退出循环,并把节点e的fail指针指向root,对应图-2中的(3),然后节点e进入队列;第3次循环时,弹出的第一个节点a的操作与上一步操作的节点e相同,把a的fail指针指向root,对应图-2中的(4),并入队;第4次进入循环时,弹出节点h(图中左边那个),这时操作略有不同。在程序运行到14行时,由于p->next[i]!=NULL(root有h这个儿子节点,图中右边那个),这样便把左边那个h节点的失败指针指向右边那个root的儿子节点h,对应图-2中的(5),然后h入队。以此类推:在循环结束后,所有的失败指针就是图-2中的这种形式。



  

最后,我们便可以在AC自动机上查找模式串中出现过哪些单词了。匹配过程分两种情况:(1)当前字符匹配,表示从当前节点沿着树边有一条路径可以到达目标字符,此时只需沿该路径走向下一个节点继续匹配即可,目标字符串指针移向下个字符继续匹配;(2)当前字符不匹配,则去当前节点失败指针所指向的字符继续匹配,匹配过程随着指针指向root结束。重复这2个过程中的任意一个,直到模式串走到结尾为止。


 1  int  query(node  * root){ 
 2       int  i = 0 ,cnt = 0 ,index,len = strlen(str); 
 3      node  * p = root;  
 4       while (str[i]){  
 5          index = str[i] - ' a ' ;  
 6           while (p -> next[index] == NULL  &&  p != root) p = p -> fail; 
 7          p = p -> next[index]; 
 8          p = (p == NULL) ? root:p; 
 9          node  * temp = p; 
10           while (temp != root  &&  temp -> count !=- 1 ){ 
11              cnt += temp -> count; 
12              temp -> count =- 1
13              temp = temp -> fail; 
14          } 
15          i ++ ;                 
16      }    
17       return  cnt; 
18  }

   

    对照图-2,看一下模式匹配这个详细的流程,其中模式串为yasherhs。对于i=0,1。Trie中没有对应的路径,故不做任何操作;i=2,3,4时,指针p走到左下节点e。因为节点e的count信息为1,所以cnt+1,并且讲节点e的count值设置为-1,表示改单词已经出现过了,防止重复计数,最后temp指向e节点的失败指针所指向的节点继续查找,以此类推,最后temp指向root,退出while循环,这个过程中count增加了2。表示找到了2个单词she和he。当i=5时,程序进入第5行,p指向其失败指针的节点,也就是右边那个e节点,随后在第6行指向r节点,r节点的count值为1,从而count+1,循环直到temp指向root为止。最后i=6,7时,找不到任何匹配,匹配过程结束。

    到此为止AC自动机算法的详细过程已经全部介绍结束,看一道例题:http://acm.hdu.edu.cn/showproblem.php?pid=2222

 

Problem Description

In the modern time, Search engine came into the life of everybody like Google, Baidu, etc.
Wiskey also wants to bring this feature to his image retrieval system.
Every image have a long description, when users type some keywords to find the image, the system will match the keywords with description of image and show the image which the most keywords be matched.
To simplify the problem, giving you a description of image, and some keywords, you should tell me how many keywords will be match.

 

 

Input

First line will contain one integer means how many cases will follow by.
Each case will contain two integers N means the number of keywords and N keywords follow. (N <= 10000)
Each keyword will only contains characters 'a'-'z', and the length will be not longer than 50.
The last line is the description, and the length will be not longer than 1000000.

 

 

Output

Print how many keywords are contained in the description.

 

 

Sample Input

1
5
she
he
say
shr
her
yasherhs

 

 

Sample Output

3

 

 1  #include  < iostream >  
 2  using   namespace  std; 
 3    
 4  const   int  kind  =   26
 5  struct  node{  
 6      node  * fail;        // 失败指针
 7      node  * next[kind];  // Tire每个节点的26个子节点(最多26个字母)
 8       int  count;         // 是否为该单词的最后一个节点
 9      node(){            // 构造函数初始化
10          fail = NULL; 
11          count = 0
12          memset(next,NULL, sizeof (next)); 
13      } 
14  } * q[ 500001 ];           // 队列,方便用于bfs构造失败指针
15  char  keyword[ 51 ];      // 输入的单词
16  char  str[ 1000001 ];     // 模式串
17  int  head,tail;         // 队列的头尾指针
18    
19  void  insert( char   * str,node  * root){ 
20      node  * p = root; 
21       int  i = 0 ,index;  
22       while (str[i]){ 
23          index = str[i] - ' a '
24           if (p -> next[index] == NULL) p -> next[index] = new  node();  
25          p = p -> next[index];
26          i ++ ;
27      } 
28      p -> count ++
29 
30  void  build_ac_automation(node  * root){
31       int  i;
32      root -> fail = NULL; 
33      q[head ++ ] = root; 
34       while (head != tail){ 
35          node  * temp = q[tail ++ ]; 
36          node  * p = NULL; 
37           for (i = 0 ;i < 26 ;i ++ ){ 
38               if (temp -> next[i] != NULL){ 
39                   if (temp == root) temp -> next[i] -> fail = root;                 
40                   else
41                      p = temp -> fail; 
42                       while (p != NULL){  
43                           if (p -> next[i] != NULL){ 
44                              temp -> next[i] -> fail = p -> next[i]; 
45                               break
46                          } 
47                          p = p -> fail; 
48                      } 
49                       if (p == NULL) temp -> next[i] -> fail = root; 
50                  } 
51                  q[head ++ ] = temp -> next[i];  
52              } 
53          }   
54      } 
55 
56  int  query(node  * root){ 
57       int  i = 0 ,cnt = 0 ,index,len = strlen(str); 
58      node  * p = root;  
59       while (str[i]){  
60          index = str[i] - ' a ' ;  
61           while (p -> next[index] == NULL  &&  p != root) p = p -> fail; 
62          p = p -> next[index]; 
63          p = (p == NULL) ? root:p; 
64          node  * temp = p; 
65           while (temp != root  &&  temp -> count !=- 1 ){ 
66              cnt += temp -> count; 
67              temp -> count =- 1
68              temp = temp -> fail; 
69          } 
70          i ++ ;                 
71      }    
72       return  cnt; 
73 
74  int  main(){ 
75       int  n,t; 
76      scanf( " %d " , & t); 
77       while (t -- ){  
78          head = tail = 0
79          node  * root = new  node(); 
80          scanf( " %d " , & n); 
81          getchar(); 
82           while (n -- ){ 
83              gets(keyword); 
84              insert(keyword,root); 
85          } 
86          build_ac_automation(root); 
87          scanf( " %s " ,str); 
88          printf( " %d/n " ,query(root));  
89      } 
90       return   0
91  }

 

 

相关参考:

http://archive.cnblogs.com/a/2045211/

http://blogold.chinaunix.net/u3/113538/showart_2212716.html

 


文章来源:http://www.cppblog.com/mythit/archive/2009/04/21/80633.html

 


 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值