AC自动机基础

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

原文地址:https://www.cnblogs.com/-Ackerman/p/11291817.html

时间: 2024-11-06 14:03:28

AC自动机基础的相关文章

ac自动机基础模板(hdu2222)

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, th

NYOJ 1085 AC自动机基础模板

今天学了AC自动机,可以说AC自动机是把匹配的串建立成为一颗trie,然后就和kmp 是一样的 题意:判断在一篇文章中有多少单词出现过,并输出来 #include<cstdio> #include<cstring> #include<map> #include<queue> #include<iostream> using namespace std; const int maxn = 1000007; int cnt; struct Node{

hdoj 2896 病毒侵袭 【AC自动机 基础题】

病毒侵袭 Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) Total Submission(s): 15893    Accepted Submission(s): 4055 Problem Description 当太阳的光辉逐渐被月亮遮蔽,世界失去了光明,大地迎来最黑暗的时刻....在这样的时刻,人们却异常兴奋--我们能在有生之年看到500年一遇的世界奇观,那是多么幸福的事

hdoj 3065 病毒侵袭持续中 【AC自动机 基础题】【输出每个模式串出现的次数】

病毒侵袭持续中 Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) Total Submission(s): 8678    Accepted Submission(s): 3045 Problem Description 小t非常感谢大家帮忙解决了他的上一个问题.然而病毒侵袭持续中.在小t的不懈努力下,他发现了网路中的"万恶之源".这是一个庞大的病毒网站,他有着好多好多

【hdu2222-Keywords Search】AC自动机基础裸题

http://acm.hust.edu.cn/vjudge/problem/16403 题意:给定n个单词,一个字符串,问字符串中出现了多少个单词.(若单词her,he,字符串aher中出现了两个单词) 题解: 每个单词末尾节点sum=1:find的时候每个点都顺着fail往上跳,加上该节点的sum,然后将这个sum清了:注意同一个单词出现多次只算一次. 1 #include<cstdio> 2 #include<cstdlib> 3 #include<cstring>

Keywords Search---hdu2222(AC自动机 模板)

题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=2222 一个常见的例子就是给出n个单词,再给出一段包含m个字符的文章,让你找出有多少个单词在文章里出现过: 本题就是最基础的模板:在此之前需要理解kmp和字典树(trie); Trie树有3个基本性质: (1) 根节点不包含字符,除根节点外每一个节点都只包含一个字符: (2) 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串: (3) 每个节点的所有子节点包含的字符都不相同. 1.

[算法模版]AC自动机

[算法模版]AC自动机 基础内容 板子不再赘述,OI-WIKI有详细讲解. \(query\)函数则是遍历文本串的所有位置,在文本串的每个位置都沿着\(fail\)跳到根,将沿途所有元素答案++.意义在于累计所有以当前字符为结尾的所有模式串的答案.看代码就能很容易的理解. 另外\(e[i]\)记录的是第\(t\)个模式串结尾是哪个节点(所有节点均有唯一的编号). 贴个P5357 [模板]AC自动机(二次加强版)板子: #include<iostream> #include<cstdio&

hdu 5384 Danganronpa(基础AC自动机)

题意:多个模式串和多个待匹配串,求每个待匹配串对于所有模式串的匹配个数: 思路:1.与最裸的ac自动机的区别在于讯问后的叶子节点的count值会改变,在每次询问时count值不要清零: 2.对于多个串的保存直接用二维数组: #include<cstdio> #include<vector> #include<iostream> #include<algorithm> #include<cstring> using namespace std; c

hdu2222 Keywords Search &amp; AC自动机学习小结

传送门:http://http://acm.hdu.edu.cn/showproblem.php?pid=2222 思路:AC自动机入门题,直接上AC自动机即可. 对于构建AC自动机,我们要做的只有三件事: 1)构建字典树 2)构建失败指针 3)构建trie图(这道题好像不做这一步也能A...但是这一步不做是会被卡成O(n^2)的...) 1)第一步还是比较好理解的 根是虚根,边代表字母,那么根到终止节点的路径就是一个字符串,这样对于前缀相同的字符串我们就可以省下存公共前缀的空间. 加入一个模式