写在前面

鸣谢:

OiWiki

「笔记」AC 自动机---LuckyBlock

字符串四姐妹---老色批

AC自动机讲解超详细---某不知名大佬

Q:AC自动机?是能自己AC题目的算法吗?(兴奋)

A:不不不,那叫自动AC机,通过打开答案文件输出答案的一种小手段,在比赛中使用还会有禁赛三年的奖励,而AC自动机是一个字符串匹配算法

AC自动机,全称\(Aho-Corasick\ automaton\),是一种用来处理字符串多模式匹配的算法

本人将尽可能详细的解释AC自动机的算法流程(其实大部分抄的Oiwiki,这是一个帮助我们共同理解的过程,毕竟作者也是个萌新。开始接受的过程可能比较困难,但多回顾几遍还是有助于理解的


算法流程


前置知识:Trie树以及KMP算法的思想

什么是自动机?(粘个链接,感性理解就好,不要过于执着)


引例:

给定 \(n\) 个模式串 \(s_i\) 和一个文本串 \(t\),求有多少个不同的模式串在文本串里出现过。

两个模式串不同当且仅当他们编号不同。

概述:

结合Trie的结构KMP的思想建立,建立一个AC自动机主要通过两个步骤:

  • 1、建立Trie树;

  • 2、对Trie树上的所有结点构造失配指针

Trie树的构建(第一步)

这个Trie树就是普通的Trie树,该怎么建怎么建

解释一下Trie树结点的含义:表示某个模式串的前缀

后文也将称作状态。一个结点表示一个状态,Trie树的边就是状态的转移

形式化的说,对于若干个模式串 \(s_1,s_2,s_3···s_n\),将它们构建一个Trie树后的所有状态的集合记为 \(Q\)

失配指针(第二步)

AC 自动机利用一个 fail 指针来辅助多模式串的匹配。

状态 \(u\) 的 fail 指针指向另一个状态 \(v\) ,其中 \(v \in Q\) ,且 \(v\) 是 \(u\) 的最长后缀(即在若干个后缀状态中取最长的一个作为 fail 指针)。

注意和KMP的next指针的区别:

两者都是在失配的时候用于跳转的指针;

next指针求的是最长的border(最长的 相同的 前后缀),而fail指针指向所有模式串的前缀中匹配当前状态的最长后缀

因为 KMP 只对一个模式串做匹配,而 AC 自动机要对多个模式串做匹配。有可能 fail 指针指向的结点对应着另一个模式串,两者前缀不同。

AC 自动机在做匹配时,同一位上可匹配多个模式串。

构建失配指针

(可以参考KMP中构建next指针的思想(

考虑更新 \(fail_u\),\(u\) 的父节点是 \(p\) , \(p\) 通过字符 \(c\) 的边指向 \(u\) ,即 \(tr[p,c] = u\) 。假设深度小于 \(u\) 的所有结点的 \(fail\) 指针均已求得。

如果 \(tr[fail_p,c]\) 存在:则让 \(fail_u\) 指向 \(tr[fail[p],c]\) 。相当于在 \(p\) 和 \(fail\) 后面加一个字符 c ,分别对应 \(u\) 和 \(fail_u\) 。

如果 \(tr[fail_p,c]\) 不存在:那么我们继续找到 \(tr[fail_{fail_p},c],c]\) 。重复 \(1\) 的判断过程,一直跳 \(fail_u\) 指针指到根结点。

如果真的没有,就让 \(fail_u\) 指针指向根结点。

这样就完成了 \(fail\) 的构建,并得到一份比较暴力的构建方式,我们来看优化

字典树和字典图

先来看构建函数 build() ,该函数的目标有两个,一个是构建 fail 指针,一个是构建自动机。

void build(){
for(int i = 0; i < 26; ++i) if(tr[0][i]) q.push(tr[0][i]);
//如果存在这个边就入队
while(!q.empty()){
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], q.push(tr[u][i]);
//按照上面所说的方式更新fail指针
else tr[u][i] = tr[fail[u]][i];//这是那个优化,后面会讲
}
}
}

原来的构建方法可以通过 \(while\) 循环寻找 \(fail\) 结点实现,循环太多次导致复杂度太高

上面提到的优化就是通过else语句的代码修改了字典树的结构。

而它将不存在的字典树状态链连接到失配指针的对应状态。使得再次遍历这里的时候会继续向下跳转,起到一个通过继续开链来压缩路径的效果,这样就能节省很多时间。

这样AC 自动机修改字典树结构连出的边就会使字典树变为字典图

会不会影响原树?在原字典树中,每一个结点代表一个字符串 ,是某个模式串的前缀。而在修改字典树结构后,尽管增加了许多转移关系,但结点(状态)所代表的字符串是不变的。

多模式匹配

(这只是对于引例的query函数,具体题目的函数写法可能不太相同)

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;
}

这里 \(u\) 作为字典树上当前匹配到的结点, \(res\) 即返回的答案。循环遍历匹配串, \(u\) 在字典树上跟踪当前字符。利用 \(fail\) 指针找出所有匹配的模式串,累加到答案中。然后清零。对 \(cnt[j]\) 取反的操作用来判断 \(cnt[j]\) 是否等于 \(-1\)。在上文中我们分析过,字典树的结构其实就是一个 \(trans\) 函数,而构建好这个函数后,在匹配字符串的过程中,我们会舍弃部分前缀达到最低限度的匹配。\(fail\) 指针则指向了更多的匹配状态。

例题

P3808 【模板】AC自动机(简单版)

P3796 【模板】AC自动机(加强版)

P5357 【模板】AC自动机(二次加强版)

算法总结篇---AC自动机的更多相关文章

  1. 算法竞赛模板 AC自动机

    AC自动机基本操作 (1) 在AC自动机中,我们首先将每一个模式串插入到Trie树中去,建立一棵Trie树,然后构建fail指针. (2) fail指针,是穿插在Trie树中各个结点之间的指针,顾名思 ...

  2. AC自动机算法小结

    AC自动机,可惜不能自动AC 转载:飘过的小牛 OIer55242 简介 Aho-Corasick automation 该算法在1975年产生于贝尔实验室,是著名的多模匹配算法之一.一个常见的例子就 ...

  3. AC自动机算法

    AC自动机简介:  首先简要介绍一下AC自动机:Aho-Corasick automation,该算法在1975年产生于贝尔实验室,是著名的多模匹配算法之一.一个常见的例子就是给出n个单词,再给出一段 ...

  4. 笔试算法题(45):简介 - AC自动机(Aho-Corasick Automation)

    议题:AC自动机(Aho-Corasick Automation) 分析: 此算法在1975年产生于贝尔实验室,是著名的多模式匹配算法之一:一个常见的例子就是给定N个单词,给定包含M个字符的文章,要求 ...

  5. 转载 - AC自动机算法

    出处:http://blog.csdn.net/niushuai666/article/details/7002823 AC自动机简介:  首先简要介绍一下AC自动机:Aho-Corasick aut ...

  6. [知识点]Trie树和AC自动机

    // 此博文为迁移而来,写于2015年5月27日,不代表本人现在的观点与看法.原始地址:http://blog.sina.com.cn/s/blog_6022c4720102w1s8.html 1.前 ...

  7. AC自动机基础知识讲解

    AC自动机 转载自:小白 还可参考:飘过的小牛 1.KMP算法: a. 传统字符串的匹配和KMP: 对于字符串S = ”abcabcabdabba”,T = ”abcabd”,如果用T去匹配S下划线部 ...

  8. HDU 2222:Keywords Search(AC自动机模板)

    http://acm.hdu.edu.cn/showproblem.php?pid=2222 KMP是单模式串匹配的算法,而AC自动机是用于多模式串匹配的算法.主要由Trie和KMP的思想构成. 题意 ...

  9. 【暑假】[实用数据结构] AC自动机

    Aho-Corasick自动机  算法: <功能> AC自动机用于解决文本一个而模板有多个的问题. AC自动机可以成功将多模板匹配,匹配意味着算法可以找到每一个模板在文本中出现的位置. & ...

随机推荐

  1. [leetcode]222. Count Complete Tree Nodes完全二叉树的节点数

    /* 满二叉树的特点是2^n-1,对于完全二叉树,一个node如果左右子树深度相同,那么 是一个满二叉树.如果不是,那就把node算上,继续往下看,下边的可能是满二叉树 由于完全二叉树中有一些子满二叉 ...

  2. JDBC(五)—— 批量插入数据

    批量插入数据 @Test public void testInsert() throws Exception { Connection conn = null; PreparedStatement p ...

  3. C++模板元编程----堆排序

    目录 目录 前言 实现的一些小细节 Debug 惰性求值 总结 Ref 前言 经过前两次经验的积累,终于来到了麻烦的堆排序.在一开始接触模板元编程的时候,我就期望有一天能够写出元编程堆排序的代码.原因 ...

  4. Liunx运维(八)-LIunx磁盘与文件系统管理命令

    文档目录: 一.fdisk:磁盘分区工具 二.partprobe:更新内核的硬盘分区表信息 三.tune2fs:调整ext2/ext3/ext4文件系统参数 四.parted:磁盘分区工具 五.mkf ...

  5. linux 笔记的注意事项

    声明:本人Linux的笔记是根据<鸟哥私房菜>而写的 command [-option] parameter1 parameter2 ... command 是命令的名称: [ ]中括号是 ...

  6. 上班从换一张桌面壁纸开始——开源小工具Bing每日壁纸

    发布一个自用的开源小软件,Bing每日壁纸,使用c# winform开发.该小软件可以自动获取Bing的精美图片设置为壁纸,并且支持随机切换历史壁纸,查看壁纸故事. 功能特性 自动获取Bing最新图片 ...

  7. JavaScript 内存详解 & 分析指南

    前言 JavaScript 诞生于 1995 年,最初被设计用于网页内的表单验证. 这些年来 JavaScript 成长飞速,生态圈日益壮大,成为了最受程序员欢迎的开发语言之一.并且现在的 JavaS ...

  8. 敏捷史话(三):笃定前行的勇者——Ken Schwaber

    很多人之所以平凡,并不在于能力的缺失,而是因为缺乏迈出一步的勇气.只有少部分的人可以带着勇气和坚持,走向不凡.Ken Schwaber 就是这样的人,他带着他的勇气和坚持在敏捷的道路上不断前行,以实现 ...

  9. Go语言从入门到放弃(设置 go get 为国内源)

    前言 Go语言学到 Gin 框架了, 其实每天学习是比较辛苦的事情, 坚持下去! 在使用 Go 过程中发现, 最无奈的是Go的一些模块下不下来, 即便挂了V, 油管2k不卡的那种, 依旧是 time ...

  10. MySQL45讲:一条update语句是怎样执行的

    首先创建一张表: create table T(ID int primary key,c int); 如果要更新ID=2这行+1:应该这样写 update T set c=c+1 where ID=2 ...