月更博主又来送温暖啦QwQ

今天我们学习的算法是AC自动机。AC自动机是解决字符串多模匹配问题的利器,而且代码也十分好打=w=

在这一篇博客里,我将讲解AC自动机是什么,以及怎么构建一个最朴素的AC自动机。(不知道为什么我写出来的AC自动机常数就是大得要命=。=)

前置知识

首先你一定要对Trie树以及KMP了如指掌,尤其是要明白KMP中失配数组(next或fail数组)的本质:利用已经匹配过的部分,跳过重复的匹配,达到快速匹配的目的。

AC自动机是什么

大家都知道KMP可以用于在一个大字符串(文本串)中寻找另一个小的字符串(模式串),那么如果有n个模式串,要你把它们全部在文本串中找出来呢?当然,我们可以做n次KMP(别小瞧30分哦),但是其效率并不能差强人意。这个时候,我们可以尝试把模式串做成Trie树,似乎可以提高效率。

比如说,我们有5个模式串:she,shr,say,he,her,那么它们所建出来的Trie树应该是长成这样的:(红色节点表示单词的结尾)

那么,怎么用它来匹配呢?如果我们把文本串的每一个点都作为起点放到Tire树上匹配,它的复杂度将会是...我要你Tire树有何用(╯‵□′)╯︵┻━┻

既然这样,那么如果只把文本串的第一个字符为起点,会发生什么呢?

你以为会是这样的:

完美!

然而实际上却是这样的:

问题很明显,当我们匹配完she时,he其实也被匹配到了。所以我们希望这棵Trie树上能够加点东西,让它可以达到下面的效果:

上图中,红色的箭头就是失配指针——fail指针。它表示文本串在当前节点失配后,我们应该到哪个节点去继续匹配。很显然,对于每个节点,我们要找到这个节点-代表的字符串-在树上所有的节点-表示的字符串中-能找到的最长的后缀,意思就是“我当前匹配到了这个点,我也相当于匹配到了的节点(中的深度最大的节点)。”比如说,在我举的例子中,当我们匹配到了she时,我们在树上走的路径也包含了he,he是she的一个后缀。我们在she上失配,至少说明我们已经匹配到了he,于是就可以跳到代表he的节点上继续匹配。

到这里,你是不是发现fail指针和KMP中的next指针简直一毛一样?它们都被称为“失配指针”。将Trie树上的每一个点都加上fail指针,它就变成了AC自动机。AC自动机其实就是Trie+KMP,它可以用来解决在文本串中寻找很多模式串,即多模匹配问题。

对于一开始的5个单词,它们所构建出的AC自动机就长这样(没有画出红色箭头的点,其fail指针都指向根节点):

如何构建AC自动机

显然,我们要做的就是快速地求出所有点的fail指针。我们以bfs的顺序依次求出每个节点的fail,这样,当我们要求一个节点的fail时,它的父亲的fail肯定已经求出来了。若当前节点为A,其父节点为B,B的fail为C,那么C所代表的字符串一定是B的最长的后缀。如果C有一个儿子D的字符与A的字符等同,那么显然D所代表的串(C加一个字符)就是A所代表的串(B加一个字符)的最长后缀。如果C没有一个儿子,使其字符与A的字符等同呢?很简单,只需要再访问C的fail就行了。如此反复,直到A的最长后缀找到,或者A的fail指向根节点为止。(A在Trie树中没有后缀,乖乖回到根重新匹配吧!)

为了解释得更清楚,我举一个例子。下面这幅图是我根据别的地方的图重新画的(n次转载?),出处我没找到_(:з」∠)_。节点是根据bfs序标号的。

步骤:

  1. 为了少一些特判,设置一个辅助根节点0号节点,0号节点的所有儿子都指向真正的根节点1号节点,然后将1号节点的fail指向0号节点。
  2. 找到2号节点的父亲节点的fail节点0号节点,看0号节点有没有为a的子节点。有,于是2号节点的fail指向1号节点。
  3. 找到3号节点的父亲节点的fail节点0号节点,看0号节点有没有为b的子节点。有,于是3号节点的fail指向1号节点。
  4. 找到4号节点的父亲节点的fail节点1号节点,看1号节点有没有为b的子节点。有,于是4号节点的fail指向3号节点。
  5. 同上。
  6. 同上。
  7. 同上。
  8. 找到8号节点的父亲节点的fail节点5号节点,看5号节点有没有为b的子节点。没有,于是再找到5号节点的fail节点2号节点,看2号节点有没有为b的子节点。有,于是8号节点的fail指向4号节点。

这样,一个AC自动机就做好了。

注意到由于辅助节点的存在,我们不需要做任何特判,在树上没有后缀的节点的fail指针会自动连向根节点。

构建fail指针的代码:

void build()
{
for(int i=0;i<26;++i)ch[0][i]=1;
fail[1]=0;
queue<int>q;
q.push(1);
while(!q.empty())
{
int x=q.front();q.pop();
for(int i=0;i<26;++i)
{
int c=ch[x][i];
if(!c)continue;
int fa=fail[x];
while(fa&&!ch[fa][i])fa=fail[fa];
fail[c]=ch[fa][i];
q.push(c);
}
}
}

如何利用AC自动机来查找

这个问题似乎显而易见,只要根据文本串的内容沿着Trie树的边往下走就行了,一失配就沿着fail边向上跳。

。。。

我在被大佬虐飞之前也是这么想的QwQ

fail边不只是失配指针这么简单,如果你像我刚才说的那么做的话,你就可能会面临下面这样的问题:

为了不让这种事情发生,我们每遇到一个fail指针就必须向上跳到顶,以保证不会漏过任何一个子串,就像这样:

当然,这样未免也太蠢了,于是这里又有一个小优化:如果一个节点的fail指向一个结尾节点,那么这个点也成为一个(伪)结尾节点。在匹配时,如果遇到结尾节点,就进行相应的计数处理。

进行匹配的代码:

void print(int x)
{
while(x)
{
if(end[x])
{
//计数、打印等等,视题目要求而定
}
x=fail[x];
}
} void match(char *s)
{
int len=strlen(s),now=1;
for(int i=0;i<len;++i)
{
int id=s[i]-'a';
while(now&&!ch[now][id])now=fail[now];
now=ch[now][id];
if(end[now]||en[now])print(now);
//en[now]即为伪结尾标记
}
} //记得在build中加上这句话
void build()
{
...
if(end[fail[c]]||en[fail[c]])en[c]=1;
...
}

一个被我们忽略的问题

时间复杂度???

设模式串平均长度为 $ l $ ,建树复杂度为 $ O(nl) $ ,构建fail指针为 $ O(nl) $ ,匹配时因为每次都要跳fail边,复杂度上界可以达到 $ O(ml) $ ,所以总复杂度为 $ O((n+m)l) $ !

这和暴力有什么区别(╯°Д°)╯︵┻━┻???

虽然说,这个上界应该是十分松的,但是我们想要的是能跑 $ 1e6 $ 的速度!

这个时候我们就需要优化了。。。然而我已经没时间写辣QwQ!这些就留到下一篇博客吧!

谢谢你的资瓷啦QwQ!

AC自动机学习笔记-1(怎么造一台AC自动机?)的更多相关文章

  1. AC自动机学习笔记-2(Trie图&&last优化)

    我是连月更都做不到的蒟蒻博主QwQ 考虑到我太菜了,考完noip就要退役了,所以我决定还是把博客的倒数第二篇博客给写了,也算是填了一个坑吧.(最后一篇?当然是悲怆のnoip退役记啦QAQ) 所以我们今 ...

  2. AC自动机板子题/AC自动机学习笔记!

    想知道484每个萌新oier在最初知道AC自动机的时候都会理解为自动AC稽什么的,,,反正我记得我当初刚知道这个东西的时候,我以为是什么神仙东西,,,(好趴虽然确实是个对菜菜灵巧比较难理解的神仙知识点 ...

  3. AC自动机学习笔记

    AC自动机 ----多个模板的字符串匹配 字典树Trie加上失配边构成 插入操作:ac.insert(p[i],i);构造失配函数:ac.getFail();计算文本串T中每个模板串的匹配数:ac.f ...

  4. 后缀自动机&回文自动机学习笔记

    在学了一天其实是边学边摆之后我终于大概$get$后缀自动机了,,,就很感动,于是时隔多年我终于决定再写篇学习笔记辽$QwQ$ $umm$和$FFT$学习笔记一样,这是一篇单纯的$gql$的知识总结博, ...

  5. [AC自动机][学习笔记]

    用途 AC自动机适用于一类用多个子串在模板串中匹配的字符串问题. 也就是说先给出一个模板串,然后给出一些子串.要求有多少个子串在这个模板串中出现过. KMP与trie树 其实AC自动机就是KMP与tr ...

  6. AC 自动机学习笔记

    虽然 NOIp 原地爆炸了,目前进入 AFO 状态,但感觉省选还是要冲一把,所以现在又来开始颓字符串辣 首先先复习一个很早很早就学过但忘记的算法--自动 AC AC自动机. AC 自动机能够在 \(\ ...

  7. 【AC自动机】【字符串】【字典树】AC自动机 学习笔记

    blog:www.wjyyy.top     AC自动机是一种毒瘤的方便的多模式串匹配算法.基于字典树,用到了类似KMP的思维.     AC自动机与KMP不同的是,AC自动机可以同时匹配多个模式串, ...

  8. AC算法学习笔记

    1.算法流程图 (1)    void Init() 此函数是初始化函数,用来给fail数组和goto数组初始化值. (2)    void GotoFunction(string x) 这个函数的作 ...

  9. ELK学习笔记(三)单台服务器多节点部署

    一般情况下单台服务器只会部署一个ElasticSearch node,但是在学习过程中,很多情况下会需要实现ElasticSearch的分布式效果,所以需要启动多个节点,但是学习开发环境(不想开多个虚 ...

随机推荐

  1. Luogu4783 【模板】矩阵求逆(高斯消元)

    对矩阵进行高斯消元直至消为单位矩阵,并在另一个单位矩阵上对其做同样的操作即可. 模意义下的高斯消元可以直接计算系数来避免整行的辗转相除. 还不知道有什么用. #include<iostream& ...

  2. Courses HDU - 1083 (二分匹配模板题)

    Consider a group of N students and P courses. Each student visits zero, one or more than one courses ...

  3. LCP 模板

    LCP Description 给定串 \(S\) . \(m\) 组询问 \((X, Y, L, R)\): 求 \(S[X,Y]\) 与 \(S[L,R]\) 的最长公共前缀. Input 第一行 ...

  4. Codechef Sad Pairs——圆方树+虚树+树上差分

    SADPAIRS 删点不连通,点双,圆方树 非割点:没有影响 割点:子树DP一下 有不同颜色,所以建立虚树 在圆方树上dfs时候 如果当前点是割点 1.统计当前颜色虚树上的不连通点对,树形DP即可 2 ...

  5. mysql新版本问题

    异常错误:Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.c ...

  6. Nginx反向代理2--配置文件配置

    2.1Nginx的反向代理 什么是正向代理? 1.2   使用nginx实现反向代理 Nginx只做请求的转发,后台有多个http服务器提供服务,nginx的功能就是把请求转发给后面的服务器,决定把请 ...

  7. 初识python面向对象编程

    初识python面向对象编程 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.面向过程的程序设计思想 #!/usr/bin/env python #_*_coding:utf-8 ...

  8. spring boot 事务配置

    事务的作用这里不细说,相信很多人也在工作中使用过. 那么在spring-boot是如何配置事务的,事实上非常简便. 直接贴代码吧. 首先配置数据源  myqlDataSource,这个就不说了.之前的 ...

  9. (64位)本体学习程序(ontoEnrich)系统使用说明文档

    系统运行:文件夹system下,可执行文件ontoEnrichment 概念学习 --------------------------------------------------------1.简 ...

  10. gcc初步窥探

    由于没有上过Linux编程这门课,所以Linux学得很水啊!!用来用去都是ls -al ; cd .. ;这些渣命令,尤其gcc都不知道什么东西来的,所以先学一下吧. 一.程序的编译过程 对于GUN编 ...