AC 自动机 
  
AC 自动机是 以 Trie 的结构为基础 ,结合 KMP 的思想  建立的自动机,用于解决多模式匹配等任务。
引入 
很多人在第一次看到这个东西的时侯是非常兴奋的。不过这个自动机叫作 Automaton,不是 Automation,这里的 AC 也不是 Accepted,而是 Aho–Corasick(Alfred V. Aho, Margaret J. Corasick. 1975),让萌新失望啦。切入正题。似乎在初学自动机相关的内容时,许多人难以建立对自动机的初步印象,尤其是在自学的时侯。而这篇文章就是为你们打造的。笔者在自学 AC 自动机后花费两天时间制作若干的 gif,呈现出一个相对直观的自动机形态。尽管这个图似乎不太可读,但这绝对是在作者自学的时侯,画得最认真的 gif 了。另外有些小伙伴问这个 gif 拿什么画的。笔者用 Windows 画图软件制作。
 
解释 
简单来说,建立一个 AC 自动机有两个步骤:
基础的 Trie 结构:将所有的模式串构成一棵 Trie。 
KMP 的思想:对 Trie 树上所有的结点构造失配指针。 
 
然后就可以利用它进行多模式匹配了。
字典树构建 
AC 自动机在初始时会将若干个模式串丢到一个 Trie 里,然后在 Trie 上建立 AC 自动机。这个 Trie 就是普通的 Trie,该怎么建怎么建。
这里需要仔细解释一下 Trie 的结点的含义,尽管这很小儿科,但在之后的理解中极其重要。Trie 中的结点表示的是某个模式串的前缀。我们在后文也将其称作状态。一个结点表示一个状态,Trie 的边就是状态的转移。
形式化地说,对于若干个模式串 \(s_1,s_2\dots s_n\) ,将它们构建一棵字典树后的所有状态的集合记作 \(Q\) 。
失配指针 
AC 自动机利用一个 fail 指针来辅助多模式串的匹配。
状态 \(u\)  的 fail 指针指向另一个状态 \(v\) ,其中 \(v\in Q\) ,且 \(v\)  是 \(u\)  的最长后缀(即在若干个后缀状态中取最长的一个作为 fail 指针)。这里简单对比一下这里的 fail 指针与 KMP  中的 next 指针:
共同点:两者同样是在失配的时候用于跳转的指针。 
不同点:next 指针求的是最长 Border(即最长的相同前后缀),而 fail 指针指向所有模式串的前缀中匹配当前状态的最长后缀。 
 
因为 KMP 只对一个模式串做匹配,而 AC 自动机要对多个模式串做匹配。有可能 fail 指针指向的结点对应着另一个模式串,两者前缀不同。
没看懂上面的对比不要急,你只需要知道,AC 自动机的失配指针指向当前状态的最长后缀状态即可。
AC 自动机在做匹配时,同一位上可匹配多个模式串。
构建指针 
下面介绍构建 fail 指针的 基础思想 :(强调!基础思想!基础!)
构建 fail 指针,可以参考 KMP 中构造 Next 指针的思想。
考虑字典树中当前的结点 \(u\) ,\(u\)  的父结点是 \(p\) ,\(p\)  通过字符 c 的边指向 \(u\) ,即 \(trie[p,\mathtt{c}]=u\) 。假设深度小于 \(u\)  的所有结点的 fail 指针都已求得。
如果 \(\text{trie}[\text{fail}[p],\mathtt{c}]\)  存在:则让 u 的 fail 指针指向 \(\text{trie}[\text{fail}[p],\mathtt{c}]\) 。相当于在 \(p\)  和 \(\text{fail}[p]\)  后面加一个字符 c,分别对应 \(u\)  和 \(fail[u]\) 。 
如果 \(\text{trie}[\text{fail}[p],\mathtt{c}]\)  不存在:那么我们继续找到 \(\text{trie}[\text{fail}[\text{fail}[p]],\mathtt{c}]\) 。重复 1 的判断过程,一直跳 fail 指针直到根结点。 
如果真的没有,就让 fail 指针指向根结点。 
 
如此即完成了 \(\text{fail}[u]\)  的构建。
例子 
下面放一张 GIF 帮助大家理解。对字符串 i  he  his  she  hers 组成的字典树构建 fail 指针:
黄色结点:当前的结点 \(u\) 。 
绿色结点:表示已经 BFS 遍历完毕的结点, 
橙色的边:fail 指针。 
红色的边:当前求出的 fail 指针。 
 
我们重点分析结点 6 的 fail 指针构建:
找到 6 的父结点 5,\(\text{fail}[5]=10\) 。然而 10 结点没有字母 s 连出的边;继续跳到 10 的 fail 指针,\(\text{fail}[10]=0\) 。发现 0 结点有字母 s 连出的边,指向 7 结点;所以 \(\text{fail}[6]=7\) 。最后放一张建出来的图:
字典树与字典图 
我们直接上代码吧。字典树插入的代码就不分析了(后面完整代码里有),先来看构建函数 build(),该函数的目标有两个,一个是构建 fail 指针,一个是构建自动机。参数如下:
tr[u,c]:有两种理解方式。我们可以简单理解为字典树上的一条边,即 \(\text{trie}[u,c]\) ;也可以理解为从状态(结点)\(u\)  后加一个字符 c 到达的状态(结点),即一个状态转移函数 \(\text{trans}(u,c)\) 。下文中我们将用第二种理解方式继续讲解。 
队列 q:用于 BFS 遍历字典树。 
fail[u]:结点 \(u\)  的 fail 指针。 
 
实现 
 
解释 
解释一下上面的代码:build 函数将结点按 BFS 顺序入队,依次求 fail 指针。这里的字典树根结点为 0,我们将根结点的子结点一一入队。若将根结点入队,则在第一次 BFS 的时候,会将根结点儿子的 fail 指针标记为本身。因此我们将根结点的儿子一一入队,而不是将根结点入队。
然后开始 BFS:每次取出队首的结点 u(\(\text{fail}[u]\)  在之前的 BFS 过程中已求得),然后遍历字符集(这里是 0-25,对应 a-z,即 \(u\)  的各个子节点):
如果 \(\text{trans}[u][\mathtt{i}]\)  存在,我们就将 \(\text{trans}[u][\mathtt{i}]\)  的 fail 指针赋值为 \(\text{trans}[\text{fail}[u]][\mathtt{i}]\) 。这里似乎有一个问题。根据之前的讲解,我们应该用 while 循环,不停的跳 fail 指针,判断是否存在字符 i 对应的结点,然后赋值,但是这里通过特殊处理简化了这些代码。 
否则,令 \(\text{trans}[u][\mathtt{i}]\)  指向 \(\text{trans}[\text{fail}[u]][\mathtt{i}]\)  的状态。 
 
这里的处理是,通过 else 语句的代码修改字典树的结构。没错,它将不存在的字典树的状态链接到了失配指针的对应状态。在原字典树中,每一个结点代表一个字符串 \(S\) ,是某个模式串的前缀。而在修改字典树结构后,尽管增加了许多转移关系,但结点(状态)所代表的字符串是不变的。
而 \(\text{trans}[S][\mathtt{c}]\)  相当于是在 \(S\)  后添加一个字符 c 变成另一个状态 \(S'\) 。如果 \(S'\)  存在,说明存在一个模式串的前缀是 \(S'\) ,否则我们让 \(\text{trans}[S][\mathtt{c}]\)  指向 \(\text{trans}[\text{fail}[S]][\mathtt{c}]\) 。由于 \(\text{fail}[S]\)  对应的字符串是 \(S\)  的后缀,因此 \(\text{trans}[\text{fail}[S]][\mathtt{c}]\)  对应的字符串也是 \(S'\)  的后缀。
换言之在 Trie 上跳转的时侯,我们只会从 \(S\)  跳转到 \(S'\) ,相当于匹配了一个 \(S'\) ;但在 AC 自动机上跳转的时侯,我们会从 \(S\)  跳转到 \(S'\)  的后缀,也就是说我们匹配一个字符 c,然后舍弃 \(S\)  的部分前缀。舍弃前缀显然是能匹配的。那么 fail 指针呢?它也是在舍弃前缀啊!试想一下,如果文本串能匹配 \(S\) ,显然它也能匹配 \(S\)  的后缀。所谓的 fail 指针其实就是 \(S\)  的一个后缀集合。
tr 数组还有另一种比较简单的理解方式:如果在位置 \(u\)  失配,我们会跳转到 \(\text{fail}[u]\)  的位置。所以我们可能沿着 fail 数组跳转多次才能来到下一个能匹配的位置。所以我们可以用 tr 数组直接记录记录下一个能匹配的位置,这样就能节省下很多时间。
这样修改字典树的结构,使得匹配转移更加完善。同时它将 fail 指针跳转的路径做了压缩(就像并查集的路径压缩),使得本来需要跳很多次 fail 指针变成跳一次。
过程 
我们将之前的 GIF 图改一下:
蓝色结点:BFS 遍历到的结点 u 
蓝色的边:当前结点下,AC 自动机修改字典树结构连出的边。 
黑色的边:AC 自动机修改字典树结构连出的边。 
红色的边:当前结点求出的 fail 指针 
黄色的边:fail 指针 
灰色的边:字典树的边 
 
可以发现,众多交错的黑色边将字典树变成了 字典图 。图中省略了连向根结点的黑边(否则会更乱)。我们重点分析一下结点 5 遍历时的情况。我们求 \(\text{trans}[5][s]=6\)  的 fail 指针:
本来的策略是找 fail 指针,于是我们跳到 \(\text{fail}[5]=10\)  发现没有 s 连出的字典树的边,于是跳到 \(\text{fail}[10]=0\) ,发现有 \(\text{trie}[0][s]=7\) ,于是 \(\text{fail}[6]=7\) ;但是有了黑边、蓝边,我们跳到 \(\text{fail}[5]=10\)  之后直接走 \(\text{trans}[10][s]=7\)  就走到 \(7\)  号结点了。
这就是 build 完成的两件事:构建 fail 指针和建立字典图。这个字典图也会在查询的时候起到关键作用。
多模式匹配 
接下来分析匹配函数 query():
实现 
解释 
这里 \(u\)  作为字典树上当前匹配到的结点,res 即返回的答案。循环遍历匹配串,\(u\)  在字典树上跟踪当前字符。利用 fail 指针找出所有匹配的模式串,累加到答案中。然后清零。在上文中我们分析过,字典树的结构其实就是一个 trans 函数,而构建好这个函数后,在匹配字符串的过程中,我们会舍弃部分前缀达到最低限度的匹配。fail 指针则指向了更多的匹配状态。最后上一份图。对于刚才的自动机:
我们从根结点开始尝试匹配 ushersheishis,那么 \(p\)  的变化将是:
红色结点:\(p\)  结点 
粉色箭头:\(p\)  在自动机上的跳转, 
蓝色的边:成功匹配的模式串 
蓝色结点:示跳 fail 指针时的结点(状态)。 
 
效率优化 
题目请参考洛谷 P5357【模板】AC 自动机(二次加强版) 
因为我们的 AC 自动机中,每次匹配,会一直向 fail 边跳来找到所有的匹配,但是这样的效率较低,在某些题目中会被卡 T。
那么我们如何优化呢?首先我们需要了解 fail 指针的一个性质:一个 AC 自动机中,如果只保留 fail 边,那么剩余的图一定是一棵树。
这是显然的,因为 fail 不会成环,且深度一定比现在低,所以得证。
而我们 AC 自动机的匹配就可以转化为在 fail 树上的链求和问题。
所以我们只需要优化一下这部分就可以了。
我们这里提供两种思路。
拓扑排序优化建图 
我们浪费的时间在哪里呢?在每次都要跳 fail。如果我们可以预先记录,最后一并求和,那么效率就会优化。
于是我们按照 fail 树建图(不用真的建,只需要记录入度):
建图 
 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 void   getfail ()    // 实际上也可以叫 build 
{ 
   for   ( int   i   =   0 ;   i   <   26 ;   i ++ )   trie [ 0 ]. son [ i ]   =   1 ; 
   q . push ( 1 ); 
   trie [ 1 ]. fail   =   0 ; 
   while   ( ! q . empty ())   { 
     int   u   =   q . front (); 
     q . pop (); 
     int   Fail   =   trie [ u ]. fail ; 
     for   ( int   i   =   0 ;   i   <   26 ;   i ++ )   { 
       int   v   =   trie [ u ]. son [ i ]; 
       if   ( ! v )   { 
         trie [ u ]. son [ i ]   =   trie [ Fail ]. son [ i ]; 
         continue ; 
       } 
       trie [ v ]. fail   =   trie [ Fail ]. son [ i ]; 
       indeg [ trie [ Fail ]. son [ i ]] ++ ;    // 修改点在这里,增加了入度记录 
       q . push ( v ); 
     } 
   } 
} 
 
 
然后我们在查询的时候就可以只为找到节点的 ans 打上标记,在最后再用拓扑排序求出答案。
查询 
 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
10 
11 
12 
13 
14 
15 
16 
17 void   query ( char   * s )   { 
   int   u   =   1 ,   len   =   strlen ( s ); 
   for   ( int   i   =   0 ;   i   <   len ;   i ++ )   u   =   trie [ u ]. son [ s [ i ]   -   'a' ],   trie [ u ]. ans ++ ; 
} 
void   topu ()   { 
   for   ( int   i   =   1 ;   i   <=   cnt ;   i ++ ) 
     if   ( ! indeg [ i ])   q . push ( i ); 
   while   ( ! q . empty ())   { 
     int   fr   =   q . front (); 
     q . pop (); 
     vis [ trie [ fr ]. flag ]   =   trie [ fr ]. ans ; 
     int   u   =   trie [ fr ]. fail ; 
     trie [ u ]. ans   +=   trie [ fr ]. ans ; 
     if   ( ! ( -- indeg [ u ]))   q . push ( u ); 
   } 
} 
 
 
主函数里这么写:
int   main ()   { 
   // do_something(); 
   scanf ( "%s" ,   s ); 
   query ( s ); 
   topu (); 
   for   ( int   i   =   1 ;   i   <=   n ;   i ++ )   cout   <<   vis [ rev [ i ]]   <<   std :: endl ; 
   // do_another_thing(); 
} 
 
完整代码 
Luogu P5357【模板】AC 自动机(二次加强版) 
 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
36 
37 
38 
39 
40 
41 
42 
43 
44 
45 
46 
47 
48 
49 
50 
51 
52 
53 
54 
55 
56 
57 
58 
59 
60 
61 
62 
63 
64 
65 
66 
67 
68 
69 
70 
71 
72 
73 
74 
75 
76 
77 
78 
79 
80 
81 
82 
83 
84 
85 
86 
87 
88 
89 
90 // Code by rickyxrc | https://www.luogu.com.cn/record/115706921 
#include   <bits/stdc++.h> 
#define maxn 8000001 
using   namespace   std ; 
char   s [ maxn ]; 
int   n ,   cnt ,   vis [ maxn ],   rev [ maxn ],   indeg [ maxn ],   ans ; 
struct   trie_node   { 
   int   son [ 27 ]; 
   int   fail ; 
   int   flag ; 
   int   ans ; 
   void   init ()   { 
     memset ( son ,   0 ,   sizeof ( son )); 
     fail   =   flag   =   0 ; 
   } 
}   trie [ maxn ]; 
queue < int >   q ; 
void   init ()   { 
   for   ( int   i   =   0 ;   i   <=   cnt ;   i ++ )   trie [ i ]. init (); 
   for   ( int   i   =   1 ;   i   <=   n ;   i ++ )   vis [ i ]   =   0 ; 
   cnt   =   1 ; 
   ans   =   0 ; 
} 
void   insert ( char   * s ,   int   num )   { 
   int   u   =   1 ,   len   =   strlen ( s ); 
   for   ( int   i   =   0 ;   i   <   len ;   i ++ )   { 
     int   v   =   s [ i ]   -   'a' ; 
     if   ( ! trie [ u ]. son [ v ])   trie [ u ]. son [ v ]   =   ++ cnt ; 
     u   =   trie [ u ]. son [ v ]; 
   } 
   if   ( ! trie [ u ]. flag )   trie [ u ]. flag   =   num ; 
   rev [ num ]   =   trie [ u ]. flag ; 
   return ; 
} 
void   getfail ( void )   { 
   for   ( int   i   =   0 ;   i   <   26 ;   i ++ )   trie [ 0 ]. son [ i ]   =   1 ; 
   q . push ( 1 ); 
   trie [ 1 ]. fail   =   0 ; 
   while   ( ! q . empty ())   { 
     int   u   =   q . front (); 
     q . pop (); 
     int   Fail   =   trie [ u ]. fail ; 
     for   ( int   i   =   0 ;   i   <   26 ;   i ++ )   { 
       int   v   =   trie [ u ]. son [ i ]; 
       if   ( ! v )   { 
         trie [ u ]. son [ i ]   =   trie [ Fail ]. son [ i ]; 
         continue ; 
       } 
       trie [ v ]. fail   =   trie [ Fail ]. son [ i ]; 
       indeg [ trie [ Fail ]. son [ i ]] ++ ; 
       q . push ( v ); 
     } 
   } 
} 
void   topu ()   { 
   for   ( int   i   =   1 ;   i   <=   cnt ;   i ++ ) 
     if   ( ! indeg [ i ])   q . push ( i ); 
   while   ( ! q . empty ())   { 
     int   fr   =   q . front (); 
     q . pop (); 
     vis [ trie [ fr ]. flag ]   =   trie [ fr ]. ans ; 
     int   u   =   trie [ fr ]. fail ; 
     trie [ u ]. ans   +=   trie [ fr ]. ans ; 
     if   ( ! ( -- indeg [ u ]))   q . push ( u ); 
   } 
} 
void   query ( char   * s )   { 
   int   u   =   1 ,   len   =   strlen ( s ); 
   for   ( int   i   =   0 ;   i   <   len ;   i ++ )   u   =   trie [ u ]. son [ s [ i ]   -   'a' ],   trie [ u ]. ans ++ ; 
} 
int   main ()   { 
   scanf ( "%d" ,   & n ); 
   init (); 
   for   ( int   i   =   1 ;   i   <=   n ;   i ++ )   scanf ( "%s" ,   s ),   insert ( s ,   i ); 
   getfail (); 
   scanf ( "%s" ,   s ); 
   query ( s ); 
   topu (); 
   for   ( int   i   =   1 ;   i   <=   n ;   i ++ )   cout   <<   vis [ rev [ i ]]   <<   std :: endl ; 
   return   0 ; 
} 
 
 
子树求和 
和拓扑排序的思路接近,我们预先将子树求和,询问时直接累加和值即可。
完整代码请见总结模板 3。
AC 自动机上 DP 
这部分将以 P2292 [HNOI2004] L 语言  为例题讲解。
一看题,不难想到一个 naive 的思路:建立 AC 自动机,在 AC 自动机上对于所有 fail 指针的子串转移,最后取最大值得到答案。
主要代码如下(若不熟悉代码中的类型定义可以跳到末尾的完整代码):
查询部分主要代码 
 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
10 
11 
12 
13 void   query ( char   * s )   { 
   int   u   =   1 ,   len   =   strlen ( s ),   l   =   0 ; 
   for   ( int   i   =   0 ;   i   <   len ;   i ++ )   { 
     int   v   =   s [ i ]   -   'a' ; 
     int   k   =   trie [ u ]. son [ v ]; 
     while   ( k   >   1 )   { 
       if   ( trie [ k ]. flag   &&   ( dp [ i   -   trie [ k ]. len ]   ||   i   -   trie [ k ]. len   ==   -1 )) 
         dp [ i ]   =   dp [ i   -   trie [ k ]. len ]   +   trie [ k ]. len ; 
       k   =   trie [ k ]. fail ; 
     } 
     u   =   trie [ u ]. son [ v ]; 
   } 
} 
 
 
主函数里取 max 即可。
for   ( int   i   =   0 ,   e   =   strlen ( T );   i   <   e ;   i ++ )   mx   =   std :: max ( mx ,   dp [ i ]); 
 
但是这样的思路复杂度不是线性(因为要跳每个节点的 fail),会被 subtask#2 卡到 T,所以我们需要一个优化的思路。
我们再看看题目的特殊性质,我们发现所有单词的长度只有 \(20\) ,所以可以想到状态压缩优化。
具体怎么优化呢?我们发现,目前的时间瓶颈主要在跳 fail 这一步,如果我们可以将这一步优化到 \(O(1)\) ,就可以保证整个问题在严格线性的时间内被解出。
那我们就将前 \(20\)  位字母中,可能的子串长度存下来,并压缩到状态中,存在每个子节点中。
那么我们在 buildfail 的时候就可以这么写:
构建 fail 指针 
 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 void   getfail ( void )   { 
   for   ( int   i   =   0 ;   i   <   26 ;   i ++ )   trie [ 0 ]. son [ i ]   =   1 ; 
   q . push ( 1 ); 
   trie [ 1 ]. fail   =   0 ; 
   while   ( ! q . empty ())   { 
     int   u   =   q . front (); 
     q . pop (); 
     int   Fail   =   trie [ u ]. fail ; 
     // 对状态的更新在这里 
     trie [ u ]. stat   =   trie [ Fail ]. stat ; 
     if   ( trie [ u ]. flag )   trie [ u ]. stat   |=   1   <<   trie [ u ]. depth ; 
     for   ( int   i   =   0 ;   i   <   26 ;   i ++ )   { 
       int   v   =   trie [ u ]. son [ i ]; 
       if   ( ! v ) 
         trie [ u ]. son [ i ]   =   trie [ Fail ]. son [ i ]; 
       else   { 
         trie [ v ]. depth   =   trie [ u ]. depth   +   1 ; 
         trie [ v ]. fail   =   trie [ Fail ]. son [ i ]; 
         q . push ( v ); 
       } 
     } 
   } 
} 
 
 
然后查询时就可以去掉跳 fail 的循环,将代码简化如下:
查询 
 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
10 
11 
12 
13 int   query ( char   * s )   { 
   int   u   =   1 ,   len   =   strlen ( s ),   mx   =   0 ; 
   unsigned   st   =   1 ; 
   for   ( int   i   =   0 ;   i   <   len ;   i ++ )   { 
     int   v   =   s [ i ]   -   'a' ; 
     u   =   trie [ u ]. son [ v ]; 
     // 因为往下跳了一位每一位的长度都+1 
     st   <<=   1 ; 
     // 这里的 & 值是状压 dp 的使用,代表两个长度集的交非空 
     if   ( trie [ u ]. stat   &   st )   st   |=   1 ,   mx   =   i   +   1 ; 
   } 
   return   mx ; 
} 
 
 
我们的 trie[u].stat 维护的是从 u 节点开始,整条 fail 链上的长度集(因为长度集小于 32 所以不影响),而 st 则维护的是查询字符串走到现在,前 32 位(因为状态压缩自然溢出)的长度集。
& 值不为 0,则代表两个长度集的交集非空,我们此时就找到了一个匹配。
完整代码 
P2292 [HNOI2004] L 语言 
 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
36 
37 
38 
39 
40 
41 
42 
43 
44 
45 
46 
47 
48 
49 
50 
51 
52 
53 
54 
55 
56 
57 
58 
59 
60 
61 
62 
63 
64 
65 
66 
67 
68 
69 
70 
71 
72 
73 
74 
75 
76 
77 
78 
79 
80 
81 
82 
83 
84 
85 
86 
87 
88 
89 
90 
91 // Code by rickyxrc | https://www.luogu.com.cn/record/115806238 
#include   <stdio.h> 
#include   <string.h> 
#include   <queue> 
#define maxn 3000001 
char   T [ maxn ]; 
int   n ,   cnt ,   vis [ maxn ],   ans ,   m ,   dp [ maxn ]; 
struct   trie_node   { 
   int   son [ 26 ]; 
   int   fail ,   flag ,   depth ; 
   unsigned   stat ; 
   void   init ()   { 
     memset ( son ,   0 ,   sizeof ( son )); 
     fail   =   flag   =   depth   =   0 ; 
   } 
}   trie [ maxn ]; 
std :: queue < int >   q ; 
void   init ()   { 
   for   ( int   i   =   0 ;   i   <=   cnt ;   i ++ )   trie [ i ]. init (); 
   for   ( int   i   =   1 ;   i   <=   n ;   i ++ )   vis [ i ]   =   0 ; 
   cnt   =   1 ; 
   ans   =   0 ; 
} 
void   insert ( char   * s ,   int   num )   { 
   int   u   =   1 ,   len   =   strlen ( s ); 
   for   ( int   i   =   0 ;   i   <   len ;   i ++ )   { 
     // trie[u].depth = i + 1; 
     int   v   =   s [ i ]   -   'a' ; 
     if   ( ! trie [ u ]. son [ v ])   trie [ u ]. son [ v ]   =   ++ cnt ; 
     u   =   trie [ u ]. son [ v ]; 
   } 
   trie [ u ]. flag   =   num ; 
   // trie[u].stat = 1; 
   // printf("set %d stat %d\n", u-1, 1); 
   return ; 
} 
void   getfail ( void )   { 
   for   ( int   i   =   0 ;   i   <   26 ;   i ++ )   trie [ 0 ]. son [ i ]   =   1 ; 
   q . push ( 1 ); 
   trie [ 1 ]. fail   =   0 ; 
   while   ( ! q . empty ())   { 
     int   u   =   q . front (); 
     q . pop (); 
     int   Fail   =   trie [ u ]. fail ; 
     trie [ u ]. stat   =   trie [ Fail ]. stat ; 
     if   ( trie [ u ]. flag )   trie [ u ]. stat   |=   1   <<   trie [ u ]. depth ; 
     for   ( int   i   =   0 ;   i   <   26 ;   i ++ )   { 
       int   v   =   trie [ u ]. son [ i ]; 
       if   ( ! v ) 
         trie [ u ]. son [ i ]   =   trie [ Fail ]. son [ i ]; 
       else   { 
         trie [ v ]. depth   =   trie [ u ]. depth   +   1 ; 
         trie [ v ]. fail   =   trie [ Fail ]. son [ i ]; 
         q . push ( v ); 
       } 
     } 
   } 
} 
int   query ( char   * s )   { 
   int   u   =   1 ,   len   =   strlen ( s ),   mx   =   0 ; 
   unsigned   st   =   1 ; 
   for   ( int   i   =   0 ;   i   <   len ;   i ++ )   { 
     int   v   =   s [ i ]   -   'a' ; 
     u   =   trie [ u ]. son [ v ]; 
     st   <<=   1 ; 
     if   ( trie [ u ]. stat   &   st )   st   |=   1 ,   mx   =   i   +   1 ; 
   } 
   return   mx ; 
} 
int   main ()   { 
   scanf ( "%d%d" ,   & n ,   & m ); 
   init (); 
   for   ( int   i   =   1 ;   i   <=   n ;   i ++ )   { 
     scanf ( "%s" ,   T ); 
     insert ( T ,   i ); 
   } 
   getfail (); 
   for   ( int   i   =   1 ;   i   <=   m ;   i ++ )   { 
     scanf ( "%s" ,   T ); 
     printf ( "%d \n " ,   query ( T )); 
   } 
} 
 
 
总结 
希望大家看懂了文章。
时间复杂度:定义 \(|s_i|\)  是模板串的长度,\(|S|\)  是文本串的长度,\(|\Sigma|\)  是字符集的大小(常数,一般为 26)。如果连了 trie 图,时间复杂度就是 \(O(\sum|s_i|+n|\Sigma|+|S|)\) ,其中 \(n\)  是 AC 自动机中结点的数目,并且最大可以达到 \(O(\sum|s_i|)\) 。如果不连 trie 图,并且在构建 fail 指针的时候避免遍历到空儿子,时间复杂度就是 \(O(\sum|s_i|+|S|)\) 。
模板 1 
Luogu P3808【模板】AC 自动机(简单版) 
 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
36 
37 
38 
39 
40 
41 
42 
43 
44 
45 
46 
47 
48 
49 
50 
51 
52 
53 
54 
55 
56 
57 
58 
59 #include   <bits/stdc++.h> 
using   namespace   std ; 
const   int   N   =   1e6   +   6 ; 
int   n ; 
namespace   AC   { 
int   tr [ N ][ 26 ],   tot ; 
int   e [ N ],   fail [ N ]; 
void   insert ( char   * s )   { 
   int   u   =   0 ; 
   for   ( int   i   =   1 ;   s [ i ];   i ++ )   { 
     if   ( ! tr [ u ][ s [ i ]   -   'a' ])   tr [ u ][ s [ i ]   -   'a' ]   =   ++ tot ;    // 如果没有则插入新节点 
     u   =   tr [ u ][ s [ i ]   -   'a' ];                                // 搜索下一个节点 
   } 
   e [ u ] ++ ;    // 尾为节点 u 的串的个数 
} 
queue < int >   q ; 
void   build ()   { 
   for   ( int   i   =   0 ;   i   <   26 ;   i ++ ) 
     if   ( tr [ 0 ][ i ])   q . push ( tr [ 0 ][ i ]); 
   while   ( q . size ())   { 
     int   u   =   q . front (); 
     q . pop (); 
     for   ( int   i   =   0 ;   i   <   26 ;   i ++ )   { 
       if   ( tr [ u ][ i ])   { 
         fail [ tr [ u ][ i ]]   = 
             tr [ fail [ u ]][ i ];    // fail数组:同一字符可以匹配的其他位置 
         q . push ( tr [ u ][ i ]); 
       }   else 
         tr [ u ][ i ]   =   tr [ fail [ u ]][ i ]; 
     } 
   } 
} 
int   query ( char   * t )   { 
   int   u   =   0 ,   res   =   0 ; 
   for   ( int   i   =   1 ;   t [ i ];   i ++ )   { 
     u   =   tr [ u ][ t [ i ]   -   'a' ];    // 转移 
     for   ( int   j   =   u ;   j   &&   e [ j ]   !=   -1 ;   j   =   fail [ j ])   { 
       res   +=   e [ j ],   e [ j ]   =   -1 ; 
     } 
   } 
   return   res ; 
} 
}    // namespace AC 
char   s [ N ]; 
int   main ()   { 
   scanf ( "%d" ,   & n ); 
   for   ( int   i   =   1 ;   i   <=   n ;   i ++ )   scanf ( "%s" ,   s   +   1 ),   AC :: insert ( s ); 
   scanf ( "%s" ,   s   +   1 ); 
   AC :: build (); 
   printf ( "%d" ,   AC :: query ( s )); 
   return   0 ; 
} 
 
 
模板 2 
Luogu P3796【模板】AC 自动机(加强版) 
 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
36 
37 
38 
39 
40 
41 
42 
43 
44 
45 
46 
47 
48 
49 
50 
51 
52 
53 
54 
55 
56 
57 
58 
59 
60 
61 
62 
63 
64 
65 
66 
67 
68 
69 
70 
71 
72 
73 
74 
75 
76 
77 #include   <bits/stdc++.h> 
using   namespace   std ; 
const   int   N   =   156 ,   L   =   1e6   +   6 ; 
namespace   AC   { 
const   int   SZ   =   N   *   80 ; 
int   tot ,   tr [ SZ ][ 26 ]; 
int   fail [ SZ ],   idx [ SZ ],   val [ SZ ]; 
int   cnt [ N ];    // 记录第 i 个字符串的出现次数 
void   init ()   { 
   memset ( fail ,   0 ,   sizeof ( fail )); 
   memset ( tr ,   0 ,   sizeof ( tr )); 
   memset ( val ,   0 ,   sizeof ( val )); 
   memset ( cnt ,   0 ,   sizeof ( cnt )); 
   memset ( idx ,   0 ,   sizeof ( idx )); 
   tot   =   0 ; 
} 
void   insert ( char   * s ,   int   id )   {    // id 表示原始字符串的编号 
   int   u   =   0 ; 
   for   ( int   i   =   1 ;   s [ i ];   i ++ )   { 
     if   ( ! tr [ u ][ s [ i ]   -   'a' ])   tr [ u ][ s [ i ]   -   'a' ]   =   ++ tot ; 
     u   =   tr [ u ][ s [ i ]   -   'a' ];    // 转移 
   } 
   idx [ u ]   =   id ;    // 以 u 为结尾的字符串编号为 idx[u] 
} 
queue < int >   q ; 
void   build ()   { 
   for   ( int   i   =   0 ;   i   <   26 ;   i ++ ) 
     if   ( tr [ 0 ][ i ])   q . push ( tr [ 0 ][ i ]); 
   while   ( q . size ())   { 
     int   u   =   q . front (); 
     q . pop (); 
     for   ( int   i   =   0 ;   i   <   26 ;   i ++ )   { 
       if   ( tr [ u ][ i ])   { 
         fail [ tr [ u ][ i ]]   = 
             tr [ fail [ u ]][ i ];    // fail数组:同一字符可以匹配的其他位置 
         q . push ( tr [ u ][ i ]); 
       }   else 
         tr [ u ][ i ]   =   tr [ fail [ u ]][ i ]; 
     } 
   } 
} 
int   query ( char   * t )   {    // 返回最大的出现次数 
   int   u   =   0 ,   res   =   0 ; 
   for   ( int   i   =   1 ;   t [ i ];   i ++ )   { 
     u   =   tr [ u ][ t [ i ]   -   'a' ]; 
     for   ( int   j   =   u ;   j ;   j   =   fail [ j ])   val [ j ] ++ ; 
   } 
   for   ( int   i   =   0 ;   i   <=   tot ;   i ++ ) 
     if   ( idx [ i ])   res   =   max ( res ,   val [ i ]),   cnt [ idx [ i ]]   =   val [ i ]; 
   return   res ; 
} 
}    // namespace AC 
int   n ; 
char   s [ N ][ 100 ],   t [ L ]; 
int   main ()   { 
   while   ( ~ scanf ( "%d" ,   & n ))   { 
     if   ( n   ==   0 )   break ; 
     AC :: init ();    // 数组清零 
     for   ( int   i   =   1 ;   i   <=   n ;   i ++ ) 
       scanf ( "%s" ,   s [ i ]   +   1 ),   AC :: insert ( s [ i ],   i );    // 需要记录该字符串的序号 
     AC :: build (); 
     scanf ( "%s" ,   t   +   1 ); 
     int   x   =   AC :: query ( t ); 
     printf ( "%d \n " ,   x ); 
     for   ( int   i   =   1 ;   i   <=   n ;   i ++ ) 
       if   ( AC :: cnt [ i ]   ==   x )   printf ( "%s \n " ,   s [ i ]   +   1 ); 
   } 
   return   0 ; 
} 
 
 
模版 3 
Luogu P5357【模板】AC 自动机(二次加强版) 
  1 
  2 
  3 
  4 
  5 
  6 
  7 
  8 
  9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
 37 
 38 
 39 
 40 
 41 
 42 
 43 
 44 
 45 
 46 
 47 
 48 
 49 
 50 
 51 
 52 
 53 
 54 
 55 
 56 
 57 
 58 
 59 
 60 
 61 
 62 
 63 
 64 
 65 
 66 
 67 
 68 
 69 
 70 
 71 
 72 
 73 
 74 
 75 
 76 
 77 
 78 
 79 
 80 
 81 
 82 
 83 
 84 
 85 
 86 
 87 
 88 
 89 
 90 
 91 
 92 
 93 
 94 
 95 
 96 
 97 
 98 
 99 
100 
101 
102 
103 
104 
105 
106 
107 
108 
109 
110 
111 
112 
113 
114 
115 
116 
117 #include   <deque> 
#include   <iostream> 
void   promote ()   { 
   std :: ios :: sync_with_stdio ( 0 ); 
   std :: cin . tie ( 0 ); 
   std :: cout . tie ( 0 ); 
   return ; 
} 
typedef   char   chr ; 
typedef   std :: deque < int >   dic ; 
const   int   maxN   =   2e5 ; 
const   int   maxS   =   2e5 ; 
const   int   maxT   =   2e6 ; 
int   n ; 
chr   s [ maxS   +   10 ]; 
chr   t [ maxT   +   10 ]; 
int   cnt [ maxN   +   10 ]; 
struct   AhoCorasickAutomaton   { 
   struct   Node   { 
     int   son [ 30 ]; 
     int   val ; 
     int   fail ; 
     int   head ; 
     dic   index ; 
   }   node [ maxS   +   10 ]; 
   struct   Edge   { 
     int   head ; 
     int   next ; 
   }   edge [ maxS   +   10 ]; 
   int   root ; 
   int   ncnt ; 
   int   ecnt ; 
   void   Insert ( chr   * str ,   int   i )   { 
     int   u   =   root ; 
     for   ( int   i   =   1 ;   str [ i ];   i ++ )   { 
       if   ( node [ u ]. son [ str [ i ]   -   'a'   +   1 ]   ==   0 ) 
         node [ u ]. son [ str [ i ]   -   'a'   +   1 ]   =   ++ ncnt ; 
       u   =   node [ u ]. son [ str [ i ]   -   'a'   +   1 ]; 
     } 
     node [ u ]. index . push_back ( i ); 
     return ; 
   } 
   void   Build ()   { 
     dic   q ; 
     for   ( int   i   =   1 ;   i   <=   26 ;   i ++ ) 
       if   ( node [ root ]. son [ i ])   q . push_back ( node [ root ]. son [ i ]); 
     while   ( ! q . empty ())   { 
       int   u   =   q . front (); 
       q . pop_front (); 
       for   ( int   i   =   1 ;   i   <=   26 ;   i ++ )   { 
         if   ( node [ u ]. son [ i ])   { 
           node [ node [ u ]. son [ i ]]. fail   =   node [ node [ u ]. fail ]. son [ i ]; 
           q . push_back ( node [ u ]. son [ i ]); 
         }   else   { 
           node [ u ]. son [ i ]   =   node [ node [ u ]. fail ]. son [ i ]; 
         } 
       } 
     } 
     return ; 
   } 
   void   Query ( chr   * str )   { 
     int   u   =   root ; 
     for   ( int   i   =   1 ;   str [ i ];   i ++ )   { 
       u   =   node [ u ]. son [ str [ i ]   -   'a'   +   1 ]; 
       node [ u ]. val ++ ; 
     } 
     return ; 
   } 
   void   addEdge ( int   tail ,   int   head )   { 
     ecnt ++ ; 
     edge [ ecnt ]. head   =   head ; 
     edge [ ecnt ]. next   =   node [ tail ]. head ; 
     node [ tail ]. head   =   ecnt ; 
     return ; 
   } 
   void   DFS ( int   u )   { 
     for   ( int   e   =   node [ u ]. head ;   e ;   e   =   edge [ e ]. next )   { 
       int   v   =   edge [ e ]. head ; 
       DFS ( v ); 
       node [ u ]. val   +=   node [ v ]. val ; 
     } 
     for   ( auto   i   :   node [ u ]. index )   cnt [ i ]   +=   node [ u ]. val ; 
     return ; 
   } 
   void   FailTree ()   { 
     for   ( int   u   =   1 ;   u   <=   ncnt ;   u ++ )   addEdge ( node [ u ]. fail ,   u ); 
     DFS ( root ); 
     return ; 
   } 
}   ACM ; 
int   main ()   { 
   std :: cin   >>   n ; 
   for   ( int   i   =   1 ;   i   <=   n ;   i ++ )   { 
     std :: cin   >>   ( s   +   1 ); 
     ACM . Insert ( s ,   i ); 
   } 
   ACM . Build (); 
   std :: cin   >>   ( t   +   1 ); 
   ACM . Query ( t ); 
   ACM . FailTree (); 
   for   ( int   i   =   1 ;   i   <=   n ;   i ++ )   std :: cout   <<   cnt [ i ]   <<   '\n' ; 
   return   0 ; 
} 
 
 
拓展 
确定有限状态自动机 
如果大家理解了上面的讲解,那么作为拓展延伸,文末我们简单介绍一下 自动机  与 KMP 自动机 。(现在你再去看自动机的定义就会好懂很多啦)
有限状态自动机(Deterministic Finite Automaton,DFA)是由
状态集合 \(Q\) ; 
字符集 \(\Sigma\) ; 
状态转移函数 \(\delta:Q\times \Sigma \to Q\) ,即 \(\delta(q,\sigma)=q',\ q,q'\in Q,\sigma\in \Sigma\) ; 
一个开始状态 \(s\in Q\) ; 
一个接收的状态集合 \(F\subseteq Q\) 。 
 
组成的五元组 \((Q,\Sigma,\delta,s,F)\) 。
那这东西你用 AC 自动机理解,状态集合就是字典树(图)的结点;字符集就是 a 到 z(或者更多);状态转移函数就是 \(\text{trans}(u,c)\)  的函数(即 \(\text{trans}[u][c]\) );开始状态就是字典树的根结点;接收状态就是你在字典树中标记的字符串结尾结点组成的集合。
KMP 自动机 
KMP 自动机就是一个不断读入待匹配串,每次匹配时走到接受状态的 DFA。如果共有 \(m\)  个状态,第 \(i\)  个状态表示已经匹配了前 \(i\)  个字符。那么我们定义 \(\text{trans}_{i,c}\)  表示状态 \(i\)  读入字符 \(c\)  后到达的状态,\(\text{next}_{i}\)  表示 prefix function ,则有:
\[
\text{trans}_{i,c} =
\begin{cases}
i + 1,  & \text{if }b_{i} = c \\[2ex]
\text{trans}_{\text{next}_{i},c}, & \text{otherwise}
\end{cases}
\]
(约定 \(\text{next}_{0}=0\) )
我们发现 \(\text{trans}_{i}\)  只依赖于之前的值,所以可以跟 KMP  一起求出来。(一些细节:走到接受状态之后立即转移到该状态的 next)
时间和空间复杂度:\(O(m|\Sigma|)\) 。
对比之下,AC 自动机其实就是 Trie 上的自动机。(虽然一开始丢给你这句话可能不知所措)
  
 
 
   
   本页面最近更新: ,更新历史  
   发现错误?想一起完善? 在 GitHub 上编辑此页!  
   本页面贡献者:iamtwz, Marcythm, 383494, abc1763613206, aofall, Chrogeek, CoelacanthusHex, Dafenghh, DanJoshua, Enter-tainer, GavinZhengOI, Gesrua, Henry-ZHR, Ir1d, kenlig, ksyx, lyccrius, Menci, opsiff, orzAtalod, ouuan, partychicken, Persdre, qq2964, Ruakker, shuzhouliu, sshwy, StudyingFather, szdytom, Tiphereth-A, Xeonacid, ZXyaang, rickyxrc  
   本页面的全部内容在 CC BY-SA 4.0  和 SATA   协议之条款下提供,附加条款亦可能应用