AC自动机

本文详细介绍AC自动机的基本概念、构造方法及应用实例。通过构造Trie树和失败指针,实现高效多模式串匹配。
AC自动机简介: 

首先简要介绍一下AC自动机:Aho-Corasick automation,该算法在1975年产生于贝尔实验室,是著名的多模匹配算法之一。一个常见的例子就是给出n个单词,再给出一段包含m个字符的文章,让你找出有多少个单词在文章里出现过。要搞懂AC自动机,先得有字典树Trie和KMP模式匹配算法的基础知识。KMP算法是单模式串的字符匹配算法,AC自动机是多模式串的字符匹配算法。

AC自动机的构造:

1.构造一棵Trie,作为AC自动机的搜索数据结构。

2.构造fail指针,使当前字符失配时跳转到具有最长公共前后缀的字符继续匹配。如同 KMP算法一样, AC自动机在匹配时如果当前字符匹配失败,那么利用fail指针进行跳转。由此可知如果跳转,跳转后的串的前缀,必为跳转前的模式串的后缀并且跳转的新位置的深度(匹配字符个数)一定小于跳之前的节点。所以我们可以利用 bfs在 Trie上面进行 fail指针的求解。

3.扫描主串进行匹配。

AC自动机详讲:

我们给出5个单词,say,she,shr,he,her。给定字符串为yasherhs。问多少个单词在字符串中出现过。

一、Trie

首先我们需要建立一棵Trie。但是这棵Trie不是普通的Trie,而是带有一些特殊的性质。

首先会有3个重要的指针,分别为p, p->fail, temp。

1.指针p,指向当前匹配的字符。若p指向root,表示当前匹配的字符序列为空。(root是Trie入口,没有实际含义)。

2.指针p->fail,p的失败指针,指向与字符p相同的结点,若没有,则指向root。

3.指针temp,测试指针(自己命名的,容易理解!~),在建立fail指针时有寻找与p字符匹配的结点的作用,在扫描时作用最大,也最不好理解。

 

对于Trie树中的一个节点,对应一个序列s[1...m]。此时,p指向字符s[m]。若在下一个字符处失配,即p->next[s[m+1]] == NULL,则由失配指针跳到另一个节点(p->fail)处,该节点对应的序列为s[i...m]。若继续失配,则序列依次跳转直到序列为空或出现匹配。在此过程中,p的值一直在变化,但是p对应节点的字符没有发生变化。在此过程中,我们观察可知,最终求得得序列s则为最长公共后缀。另外,由于这个序列是从root开始到某一节点,则说明这个序列有可能是某些序列的前缀。

再次讨论p指针转移的意义。如果p指针在某一字符s[m+1]处失配(即p->next[s[m+1]] == NULL),则说明没有单词s[1...m+1]存在。此时,如果p的失配指针指向root,则说明当前序列的任意后缀不会是某个单词的前缀。如果p的失配指针不指向root,则说明序列s[i...m]是某一单词的前缀,于是跳转到p的失配指针,以s[i...m]为前缀继续匹配s[m+1]。

对于已经得到的序列s[1...m],由于s[i...m]可能是某单词的后缀,s[1...j]可能是某单词的前缀,所以s[1...m]中可能会出现单词。此时,p指向已匹配的字符,不能动。于是,令temp = p,然后依次测试s[1...m], s[i...m]是否是单词。

构造的Trie为:

二、构造失败指针

用BFS来构造失败指针,与KMP算法相似的思想。

首先,root入队,第1次循环时处理与root相连的字符,也就是各个单词的第一个字符h和s,因为第一个字符不匹配需要重新匹配,所以第一个字符都指向root(root是Trie入口,没有实际含义)失败指针的指向对应下图中的(1),(2)两条虚线;第2次进入循环后,从队列中先弹出h,接下来p指向h节点的fail指针指向的节点,也就是root;p=p->fail也就是p=NULL说明匹配序列为空,则把节点e的fail指针指向root表示没有匹配序列,对应图-2中的(3),然后节点e进入队列;第3次循环时,弹出的第一个节点a的操作与上一步操作的节点e相同,把a的fail指针指向root,对应图-2中的(4),并入队;第4次进入循环时,弹出节点h(图中左边那个),这时操作略有不同。由于p->next[i]!=NULL(root有h这个儿子节点,图中右边那个),这样便把左边那个h节点的失败指针指向右边那个root的儿子节点h,对应图-2中的(5),然后h入队。以此类推:在循环结束后,所有的失败指针就是图-2中的这种形式。

 

三、扫描

构造好Trie和失败指针后,我们就可以对主串进行扫描了。这个过程和KMP算法很类似,但是也有一定的区别,主要是因为AC自动机处理的是多串模式,需要防止遗漏某个单词,所以引入temp指针。

匹配过程分两种情况:(1)当前字符匹配,表示从当前节点沿着树边有一条路径可以到达目标字符,此时只需沿该路径走向下一个节点继续匹配即可,目标字符串指针移向下个字符继续匹配;(2)当前字符不匹配,则去当前节点失败指针所指向的字符继续匹配,匹配过程随着指针指向root结束。重复这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自动机入门知识就讲完了。HDU 2222入门题必须果断A掉。

RE代码存档:

 1 #include<iostream>
 2 #include<queue>
 3 #include<cstring>
 4 #include<cstdio>
 5 using namespace std;
 6 struct node {
 7     node *fail;
 8     node *next[26];
 9     int count;
10     node() {
11         fail=NULL;
12         count=0;
13         for(int i=0;i<26;i++)
14           next[i]=NULL;
15     }
16 }*q[500011];
17 node *root;
18 int head,tail;
19 char keyword[51],str[1000011];
20 void insert(char *str) {
21     node *p=root;
22     int i=0,index,len=strlen(str);
23     for(int i=0;i<len;i++) {
24         index=str[i]-'a';
25         if(p->next[index]==NULL)
26           p->next[index]=new node();
27         p=p->next[index];
28     }
29     p->count++;
30 }
31 void build_ac_automation() {
32     q[tail++]=root;
33     while(head!=tail) {
34         node *p=q[head++];
35         node *tmp=NULL;
36         for(int i=0; i<26; i++) {
37             if(p->next[i]!=NULL) {
38                 if(p==root) p->next[i]->fail=root;
39                 else {
40                     tmp=p->fail;
41                     while(tmp!=NULL) {
42                         if(tmp->next[i]!=NULL) {
43                             p->next[i]->fail=tmp->next[i];
44                           break;
45                         }
46                         tmp=tmp->fail;
47                     }
48                     if(tmp==NULL) p->next[i]->fail=root;
49                 }
50                 q[tail++]=p->next[i];
51             }
52         }
53     }
54 }
55 int query() {
56     int i=0,cnt=0,len=strlen(str),index;
57     node *p=root;
58     for(int i=0;i<len;i++) {
59         index=str[i]-'a';
60         while(p->next[i]==NULL && p!=root) 
61           p=p->fail;
62         p=p->next[index];
63         if(p==NULL) 
64           p=root;
65         node *tmp=p;
66         while(tmp!=root && tmp->count!=-1) {
67             cnt+=tmp->count;
68             tmp->count=-1;
69             tmp=tmp->fail;
70         }
71     }
72     return cnt;
73 }
74 int main() {
75     int n,t;
76     scanf("%d",&t);
77     while(t--) {
78         head=tail=0;
79         root=new node();
80         scanf("%d",&n);
81         getchar();
82         for(int i=0;i<n;i++){
83             gets(keyword);
84             insert(keyword);
85         }
86         build_ac_automation();
87         scanf("%s", str);
88         printf("%d\n",query());
89     }
90     return 0;
91 }

AC代码存档:

  1 #include<iostream>
  2 #include<cstdio>
  3 #include<cstring>
  4 #include<string>
  5 using namespace std;
  6 #define N 500010
  7 char str[1000010], keyword[51];
  8 int head, tail;
  9 
 10 struct node
 11 {
 12     node *fail;
 13     node *next[26];
 14     int count;
 15     node() //init
 16     {
 17         fail = NULL;
 18         count = 0;
 19         for(int i = 0; i < 26; ++i)
 20             next[i] = NULL;
 21     }
 22 }*q[N];
 23 node *root;
 24 
 25 void insert(char *str) //建立Trie
 26 {
 27     int temp, len;
 28     node *p = root;
 29     len = strlen(str);
 30     for(int i = 0; i < len; ++i)
 31     {
 32         temp = str[i] - 'a';
 33         if(p->next[temp] == NULL)
 34             p->next[temp] = new node();
 35         p = p->next[temp];
 36     }
 37     p->count++;
 38 }
 39 
 40 void build_ac() //初始化fail指针,BFS
 41 {
 42     q[tail++] = root;
 43     while(head != tail)
 44     {
 45         node *p = q[head++]; //弹出队头
 46         node *temp = NULL;
 47         for(int i = 0; i < 26; ++i)
 48         {
 49             if(p->next[i] != NULL)
 50             {
 51                 if(p == root) //第一个元素fail必指向根
 52                     p->next[i]->fail = root;
 53                 else
 54                 {
 55                     temp = p->fail; //失败指针
 56                     while(temp != NULL) //2种情况结束:匹配为空or找到匹配
 57                     {
 58                         if(temp->next[i] != NULL) //找到匹配
 59                         {
 60                             p->next[i]->fail = temp->next[i];
 61                             break;
 62                         }
 63                         temp = temp->fail;
 64                     }
 65                     if(temp == NULL) //为空则从头匹配
 66                         p->next[i]->fail = root;
 67                 }
 68                 q[tail++] = p->next[i]; //入队
 69             }
 70         }
 71     }
 72 }
 73 
 74 int query() //扫描
 75 {
 76     int index, len, result;
 77     node *p = root; //Tire入口
 78     result = 0;
 79     len = strlen(str);
 80     for(int i = 0; i < len; ++i)
 81     {
 82         index = str[i] - 'a';
 83         while(p->next[index] == NULL && p != root) //跳转失败指针
 84             p = p->fail;
 85         p = p->next[index];
 86         if(p == NULL)
 87             p = root;
 88         node *temp = p; //p不动,temp计算后缀串
 89         while(temp != root && temp->count != -1)
 90         {
 91             result += temp->count;
 92             temp->count = -1;
 93             temp = temp->fail;
 94         }
 95     }
 96     return result;
 97 }
 98 
 99 int main()
100 {
101     int ncase, num;
102     scanf("%d", &ncase);
103     while(ncase--)
104     {
105         head= tail = 0;
106         root = new node();
107         scanf("%d", &num);
108         getchar();
109         for(int i = 0; i < num; ++i)
110         {
111             gets(keyword);
112             insert(keyword);
113         }
114         build_ac();
115         scanf("%s", str);
116         printf("%d\n", query());
117     }
118     return 0;
119 }

还望哪位大神给我指点一下,两份代码有哪里不一样

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

七情六欲·

学生党不容易~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值