月更博主又来送温暖啦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. #LOJ2564 SDOI2018 原题识别 主席树

    转载请注明原文地址:http://www.cnblogs.com/LadyLex/p/9057297.html 原题链接: 今天考试考了前天的SDOI考题 天啊我菜爆,只有T2拿了30分 然后考试后半 ...

  2. bzoj2817[ZJOI2012]波浪

    题目链接: http://www.lydsy.com/JudgeOnline/problem.php?id=2817 波浪 [问题描述] 阿米巴和小强是好朋友. 阿米巴和小强在大海旁边看海水的波涛.小 ...

  3. suoi44 核能显示屏 (cdq分治)

    首先二维树状数组肯定开不下 仿照二维树状数组的做法,如果有差分数组$d[i][j]=a[i][j]-a[i-1][j]-a[i][j-1]+a[i-1][j-1]$,那么就有: $$sum[x][y] ...

  4. Linux上查找

    locate 用法:locate filename locate是Linux系统中的一个查找(定位)文件命令,和find命令等找寻文件的工作原理类似,但locate是通过生成一个文件和文件夹的索引数据 ...

  5. kubernetes配置secret拉取私仓镜像

    2017.05.10 19:48* 字数 390 阅读 5216评论 0喜欢 8 对于公司内部的项目, 我们不可能使用公有开放的镜像仓库, 一般情况可能会花钱买docker私仓服务, 或者说自己在服务 ...

  6. POJ 2932 圆扫描线

    求n个圆中没有被包含的圆.模仿扫描线从左往右扫,到左边界此时如有3个交点,则有3种情况,以此判定该圆是否被离它最近的圆包含,而交点和最近的圆可以用以y高度排序的Set来维护.因此每次到左边界插入该圆, ...

  7. ASP.NET MVC学习笔记-----Filter(1)

    Filter类型 接口 MVC的默认实现 Description Authorization IAuthorizationFilter AuthorizeAttribute 最先执行,在其他类型的fi ...

  8. 用python处理文本,本地文件系统以及使用数据库的知识基础

    主要是想通过python之流的脚本语言来进行文件系统的遍历,处理文本以及使用简易数据库的操作. 本文基于陈皓的:<程序员技术练级攻略> 一.Python csv 对于电子表格和数据库导出文 ...

  9. CMSZU站群管理系统 升级到 v1.8 [源码下载]

    CmsZu 简介 CMSZU即CMS族,是个网站内容管理平台,基于PHP+MYSQL技术创建,源码开放. CmsZu 更新说明 V1.8 修改了些bug 完善数据库管理 -> 数据库表管理的 字 ...

  10. 使用js获取浏览器地址栏里的参数

    用JS获取地址栏参数的方法(超级简单) 方法一:采用正则表达式获取地址栏参数:( 强烈推荐,既实用又方便!) function GetQueryString(name) { var reg = new ...