算法学习——AC自动机

其实算是复习了。。。

 首先在学习这个之前我们需要学会trie树这个东西,详见算法复习——trie树

  AC自动机就是在trie树的基础上建立起来的。

  先看几个定义:

    1,fail指针:这个指针指向 满足等于当前串的某一后缀的串中,深度最大的那个串。因为在trie树上任意一个点都可以代表从root到这个点所构成的串,所以假设我们现在有abcd这个串,这个指针指向的串是bcd,这就是合法的。因为bcd这个串是abcd这个串的某一后缀。

    2,fail树:我们把每个节点的fail指针看做边,建出一棵树,这棵树被称为fail树。

  fail指针有什么用呢?

    1,首先我们可以发现,如果我们要求在一个串S中有哪些字符串出现过的话,我们可以发现如果一个串x在S中出现过,那么那个串的fail指向的串也必定在S中出现过。

    2,所以我们可以直接在trie树上遍历S串,如果遇到了一个没有的节点,我们就回到root,否则下一步去它的儿子 or fail(在没有儿子的情况下才去fail)

    因为第1点,所以每当我们遍历到一个串时,我们就需要沿着它的fail树上的边(即fail指针)向上遍历,不断累加贡献。

  那么如何求fail呢?

    首先对于root的儿子而言,它们的fail肯定都是root,因为没有更短的串了QAQ。

    然后我们可以用类似DP的方式来求fail数组。

    首先我们可以观察到一个点的fail,一定是沿着它的父亲的fail向上遍历遇到的某个点的儿子。因为它父亲的fail已经满足最长这个限制了,那么我们肯定要在这个基础上尽量扩展。

    所以难道每次更新的时候我们都要暴跳fail树吗?

    其实是不需要的,因为fail肯定会指向深度更浅的点,所以我们用bfs的方式来遍历trie树。然后我们有一个巧妙的写法来实现O(1)的转移。

1         while(head<tail)
2         {
3             now=q[++head];
4             for(R i=0;i<26;i++)
5                 if(c[now][i]) fail[c[now][i]]=c[fail[now]][i],q[++tail]=c[now][i];
6                 else c[now][i]=c[fail[now]][i];//建立虚拟节点
7         }

    即每次当前节点的某个儿子为空的时候,我们强行把这个空儿子指向它fail的这个儿子,这样下次有点再需要判断这个空儿子是否存在时,如果它存在,就可以直接指向它,如果它不存在,就会被指向它再往上跳一步的那个位置的儿子。(注意也不一定就是跳1步,也可能是跳很多步,因为c[fail[now]][i]这个节点也可能就是记录了向上跳x步的一个位置,这样一步步累计下来就可能有很多步了)

    可以画图理解一下。

  

  但我们可以注意到一点,当我们建出AC自动机后,我们在AC自动机上的每一步操作,其实质都是在fail树上进行操作。

  而如果我们匹配文本串时每次都一步一步的跳fail,是可能退化到$n^2$复杂度的(在需要重复统计的情况下,类似[TJOI2013]单词,即每个串可以造成多次贡献)。

  这个时候可以分2种情况讨论:

  1,如果我们只需要记录总的贡献,我们可以对fail树上的每个节点做个记忆化,因为一旦访问到一个节点,接下来会怎么走都是固定的(父亲只有一个)。所以我们可以在访问过某个节点后记录下一旦我们访问到这个节点,我们可以得到多少贡献,然后下一次再访问到这个节点就不用再往上跳了,可以直接退出了。

  2,如果我们需要对每个小串分别记录贡献,即贡献是加在小串上面的。那么因为我们遍历到一个点,其实就是要给这个点到root的这条链上的所有点的ans都++,所以我们可以直接给这个点打上加1的标记,然后用类似O(n)求树状数组的方法在最后统计贡献。当然也可以这么考虑,因为一个点上的标记可以对当前点产生贡献当且仅当那个点是当前点的子树,所以想办法维护一下子树和之类的。

  不过更好写的方法是在最后用类似O(n)求树状数组的方法统计贡献。

  大致方法如下:

    对于一个节点x,如果我们把它的标记传给fa[x],然后我们再把fa[x]的标记传给fa[fa[x]],我们就可以看做fa[x]在上传贡献的时候顺便把x捎给它的贡献也传上去了,相当于一个人传了2份贡献,其中一份属于x,一份属于fa[x].

    那么要让fa[x]顺便把x的贡献也传上去的条件就是x要在fa[x]上传贡献之前就把贡献传给fa[x].

    那么显然我们只需要用拓扑序的顺序来上传贡献就可以保证这点了。

    又因为我们建fail树的时候是用的bfs,bfs的队列里面其实就是一个拓扑序,所以就可以直接调用原来用过的数组,而不用再求拓扑序。

    代码为luogu上面AC自动机(简单版)的模板。

 1 #include<bits/stdc++.h>
 2 using namespace std;
 3 #define R register int
 4 #define AC 500100
 5 #define ac 1001000
 6 
 7 int n, len, ans;
 8 int q[ac], head, tail;
 9 char s[ac];
10 
11 struct AC_machine{
12     int c[AC][26], fail[AC], val[AC], tot;
13         
14     inline void ins()//插入一个字符串
15     {
16         int now = 0;
17         for(R i = 1; i <= len; i ++)
18         {
19             int v = s[i] - 'a';
20             if(!c[now][v]) c[now][v] = ++ tot;
21             now = c[now][v];
22         }
23         ++ val[now];
24     }
25     
26     void build()
27     {
28         for(R i = 0; i < 26; i ++) 
29             if(c[0][i]) q[++ tail] = c[0][i];
30         int now;
31         while(head < tail)
32         {
33             now = q[++ head];
34             for(R i = 0; i < 26; i ++)
35             {
36                 if(c[now][i]) fail[c[now][i]] = c[fail[now]][i], q[++ tail] = c[now][i];
37                 else c[now][i] = c[fail[now]][i];
38             } 
39         }
40     }
41     
42     void get()
43     {
44         int now = 0;
45         for(R i = 1; i <= len; i ++)
46         {
47             now = c[now][s[i] - 'a'];
48             for(R i = now; i && ~val[i]; i = fail[i]) ans += val[i], val[i] = -1; 
49         }
50         printf("%d\n", ans);
51     }
52 }T;
53 
54 void pre()
55 {
56     scanf("%d", &n);
57     for(R i = 1; i <= n; i ++) 
58         scanf("%s", s + 1), len = strlen(s + 1), T.ins();
59     scanf("%s", s + 1), len = strlen(s + 1);
60 }
61 
62 int main()
63 {
64 //    freopen("in.in", "r", stdin);
65     pre();
66     T.build();
67     T.get();
68 //    fclose(stdin);
69     return 0;
70 }
View Code

猜你喜欢

转载自www.cnblogs.com/ww3113306/p/9985838.html
今日推荐