月更博主又来送温暖啦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. BZOJ2568 比特集合(树状数组)

    考虑维护f[k][x]表示“最低k位所表示的数不大于x”的数的个数.那么查询时答案就为f[k][2k-1]-f[k][2k-1-1]. 同时记录每个数在集合中出现多少次.这样的话插入.删除已经解决了, ...

  2. 洛谷P3348 [ZJOI2016]大森林(LCT,虚点,树上差分)

    洛谷题目传送门 思路分析 最简单粗暴的想法,肯定是大力LCT,每个树都来一遍link之类的操作啦(T飞就不说了) 考虑如何优化算法.如果没有1操作,肯定每个树都长一样.有了1操作,就来仔细分析一下对不 ...

  3. BZOJ 2561 最小生成树 | 网络流 最小割

    链接 BZOJ 2561 题解 用Kruskal算法的思路来考虑,边(u, v, L)可能出现在最小生成树上,就是说对于所有边权小于L的边,u和v不能连通,即求最小割: 对于最大生成树的情况也一样.容 ...

  4. 使用Metasploit绕过UAC的多种方法

      一.用户帐户控制(UAC)简介 在本文中,我们将简要介绍一下用户帐户控制,即UAC.我们还将研究它如何潜在地保护免受恶意软件的攻击并忽略UAC提示可能给系统带来的一些问题. 1.什么是用户帐户控制 ...

  5. Redis连接的客户端(connected_clients)数过高或者不减的问题解决方案

    最近的项目上使用ServiceStack.Redis上了redis缓存,其中遇到了很多问题.. 比如说 某一天发现redis做的缓存竟然失效了,然后查了下日志  报错max number of cli ...

  6. 9:@RequestMapping 用法详解之地址映射

    引言: 前段时间项目中用到了RESTful模式来开发程序,但是当用POST.PUT模式提交数据时,发现服务器端接受不到提交的数据(服务器端参数绑定没有加任何注解),查看了提交方式为applicatio ...

  7. D. Recovering BST Codeforces Round #505 (rated, Div. 1 + Div. 2, based on VK Cup 2018 Final)

    http://codeforces.com/contest/1025/problem/D 树 dp 优化 f[x][y][0]=f[x][z][1] & f[z+1][y][0] ( gcd( ...

  8. JavaScript模拟QQ签名(HTML5 contenteditable属性)

    例图: 一.思路 1.单击元素时,使元素可以编辑,并获得焦点 2.按下键盘检测用户编辑元素中的文本 3.监听按下Enter键操作或离开可编辑元素焦点时,更新数据库 二.代码 $(function(){ ...

  9. html5 canvas 圆形径向渐变

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...

  10. [转载]ASP.NET Error – Adding the specified count to the semaphore would cause it to exceed its maximum count

    http://jwcooney.com/2012/08/13/asp-net-error-adding-the-specified-count-to-the-semaphore-would-cause ...