AC自动机不是自动AC机

 

简介

看dalao们AC自动机的Blog,大多数奆奆都会感性地说: AC_automation = KMP+TRIE 然而在我重蹈覆辙辗转反侧n次后才明白,这东西说了等于没说。

  • AC自动机是一种有限状态自动机(说了等于没说),它常被用于多模式串的字符串匹配。
  • 在学完AC自动机,我也总结出一句说了等于没说的话: AC自动机是以TRIE的结构为基础,结合KMP的思想建立的。

 

建立AC自动机

建立一个AC自动机通常需要两个步骤:

  • 基础的TRIE结构:将所有的模式串构成一棵Trie。
  • KMP的思想:对Trie树上所有的结点构造失配指针。

然后就可以利用它进行多模式匹配了。

TRIE构建

  • 和trie的insert操作一模一样(强调!一模一样!)
  • 因为我们只利用TRIE的结构,所以只需将模式串存入即可。

 

首先用模式串建成一颗trie树 在每个模式串末尾打上标记 表示这是一个模式串的末尾

树上的每一个节点都对应一个fail指针 作用和KMP中的nex差不多 

如果我可以通过fail指针走向一个节点 那么就表明新的这个节点到根节点的前缀字符串是我这个字符串中出现过的 

可以直接从这个fail指针的节点继续匹配 大大节省了再匹配的时间

所以怎么建fail指针呢 

首先fail指针指向的节点肯定与我当前节点代表的字符是一样的 (要不然怎么可以直接继续匹配呢)

其次要保证fail指向串的前缀在前一个模式串中出现过 要做到这一点 当前点的fail指针就要用到他父亲的fail指针来进行构建

具体看图

 

我现在有三个模式串 abcd  bce cf 第一层的fail指针指向的都是root节点 那么从abcd开始看

abcd的b他的父亲a的fail是根节点 那么我们看他父亲的fail有没有指向和他相同的儿子 

发现有bce中的b 直接指向那个b 同理 abcd中的c指向他父亲的fail的相同儿子c

这时候abcd中的d找不到相同的 就指向了root 这是fail的构建过程

但是我们发现一直沿着fail指针跳时间复杂度没办法完全保证 这时候就出现了trie图

简单地说就是 如果我没有这个儿子 我就把我fail的这个儿子扯下来做我的儿子 --和fail共享儿子

重新举个简单例子 

 

我现在有模式串 abc bc cd

图中的实边都是fail指针 现在我们来在此基础上优化成trie图 abc的c没有d这个儿子

但是他又很想要一个d儿子 怎么办呢 -- 就去他的fail的儿子里面找哇 

(同样是fail因为要保证他fail那个串的前缀跟当前模板串相匹配)

但是他的fail也没有d这个儿子怎么办呢 他fail也是有fail的啊

这时候就指向了cd中的c了 太妙了 这个c有d这个儿子 这时候bc中的c就把cd中的d这下拉来当儿子了(虚边)

那么abc中的c就拉了bc的d来做儿子--也就cd的d儿子 这时候三个c共用一个d儿子

也就是说如果我的文本串是abcd  就走完abc后直接走向cd中的d了 直接保证了时间复杂度

这里有一个小细节 当我走到abc中的c时 他的fail(bc中的c)明明可以拉cd中的d下来做儿子 

但是如果我bc中的c还没来得及拉儿子怎么办呢 会不会直接就指向根节点了吗?

答案是否定的 怎么避免这个问题呢 

bfs就好了啊 我每次的fail指向的都是深度更浅的节点(想一想 为什么) 也就是说我在处理这个点时 我的fail已经在上层被处理过了

所以不会出现没来得及拉儿子的情况

其实直白点来说,优化之后的AC自动机其实就是简化了fail指针的跳动的过程,直接把fail[k]的儿子拿过来当作自己的儿子。

而没有优化的AC自动机则需要fail指针的跳动,直到找到真正的fail[k] 以及它的孩子 (这鬼东西我模拟了三个小时才明白)

 

我们先来看看没有优化的构建fail的代码:

 

 1 void inset(char *s) //和字典树一样
 2 {
 3     int len = strlen(s);
 4     int p = 0;
 5     for (int i=0;i<len;i++)
 6     {
 7         int v = s[i]-'a';
 8         if (!str[p][v])
 9         {
10             str[p][v] = cnt++;
11         }
12         p = str[p][v];
13     }
14     end[p]++;
15 }
16 
17 void build()
18 {
19     queue<int> q;
20     memset(fail,0, sizeof(fail));
21     for (int i=0;i<26;i++)
22     {
23         if (str[0][i])
24             q.push(str[0][i]);
25     }
26     while (!q.empty())
27     {
28         int k = q.front();
29         q.pop();
30         for (int i=0;i<26;i++)
31         {
32             if (str[k][i])
33             {
34                 int t = fail[k];
35                 while (t && !str[t][i])
36                     t = fail[t];
37                 fail[str[k][i]] = str[t][i];
38                 q.push(str[k][i]);
39             }
40         }
41     }
42 }
43 
44 int query(char *t)
45 {
46     int k=0;
47     int ans = 0;
48     int len = strlen(t);
49     for (int i=0;i<len;i++)
50     {
51         while (!str[k][t[i]-'a'] && k)
52             k = fail[k];
53         k = str[k][t[i]-'a'];
54         if (end[k])
55         {
56             ans++;
57             end[k] = false;
58         }
59     }
60     return ans;
61 }

 

优化之后的:

 1 #include <stdio.h>
 2 #include <algorithm>
 3 #include <iostream>
 4 #include <stdbool.h>
 5 #include <stdlib.h>
 6 #include <string>
 7 #include <string.h>
 8 #include <math.h>
 9 #include <vector>
10 #include <queue>
11 #include <stack>
12 #include <set>
13 #include <map>
14 
15 #define INF 0x3f3f3f3f
16 #define LL long long
17 #define MAXN 200005
18 using namespace std;
19 
20 const int N=1000;
21 struct AC_automaton{
22     int tr[N][26],cnt;//TRIE
23     int e[N];//标记字符串结尾
24     int fail[N];//fail指针
25 
26     void insert(char *s){//插入模式串
27         int p=0;
28         for(int i=0;s[i];i++){
29             int k=s[i]-'a';
30             if(!tr[p][k])tr[p][k]=++cnt;
31             p=tr[p][k];
32         }
33         e[p]++;
34     }
35     void build(){
36         queue<int>q;
37         memset(fail,0,sizeof(fail));
38         for(int i=0;i<26;i++)if(tr[0][i])q.push(tr[0][i]);
39         //首字符入队
40         //不直接将0入队是为了避免指向自己
41         while(!q.empty()){
42             int k=q.front();q.pop();//当前结点
43             for(int i=0;i<26;i++){
44                 if(tr[k][i]){
45                     fail[tr[k][i]]=tr[fail[k]][i];//构建当前的fail指针
46                     q.push(tr[k][i]);//入队
47                 }
48                 else tr[k][i]=tr[fail[k]][i];
49                 //匹配到空字符,则索引到父节点fail指针对应的字符,以供后续指针的构建
50                 //类似并差集的路径压缩,把不存在的tr[k][i]全部指向tr[fail[k]][i]
51                 //这句话在后面匹配主串的时候也能帮助跳转
52             }
53         }
54     }
55     int query(char *t){
56         int p=0,res=0;
57         for(int i=0;t[i];i++){
58             p=tr[p][t[i]-'a'];
59             for(int j=p;j&&~e[j];j=fail[j])res+=e[j],e[j]=-1;  //   ~e[j]是判断e[j]是否为-1
60         }
61         return res;
62     }
63 }ac;
64 
65 char s[1000005],cmp[1000005];
66 int main( ) {
67     int n;
68     scanf("%d",&n);
69     for(int i = 1;i <= n;i ++) {
70         scanf("%s",s);
71         ac.insert(s);
72     }
73     ac.build();
74     scanf("%s",cmp);
75     int ans = ac.query(cmp);
76     printf("%d",ans);
77 
78     return 0;
79 }

 

 

推荐博客:

https://www.luogu.org/blog/42196/qiang-shi-tu-xie-ac-zi-dong-ji

http://blog.c0per.org/2018-10/ac/#comments

https://blog.csdn.net/Ruben_uz/article/details/80781226