AC自动机算法及模板

2016-05-08 18:58 226人阅读 评论(0) 收藏 举报
 分类:
AC自动机(1) 

版权声明:本文为博主原创文章,未经博主允许不得转载。

目录(?)[+]

  • 关于AC自动机
  1. AC自动机:Aho-Corasickautomation,该算法在1975年产生于贝尔实验室,是著名的多模匹配算法之一。一个常见的例子就是给出n个单词,再给出一段包含m个字符的文章,让你找出有多少个单词在文章里出现过。要搞懂AC自动机,先得有模式树(字典树)Trie和KMP模式匹配算法的基础知识。AC自动机算法分为3步:构造一棵Trie树,构造失败指针和模式匹配过程。
  2. 简单来说,AC自动机是用来进行多模式匹配(单个主串,多个模式串)的高效算法。
  • AC自动机的构造过程
使用Aho-Corasick算法需要三步:
  1. 建立模式串的Trie
  2. 给Trie添加失败路径
  3. 根据AC自动机,搜索待处理的文本

我们以下面这个例子来介绍AC自动机的运作过程

这里以 hdu 2222 KeywordsSearch 这一道题最为例子进行讲解,其中测试数据如下:

给定5个单词:say she shr he her,然后给定一个字符串  yasherhs。问一共有多少单词在这个字符串中出现过。

  • 确定数据结构
首先,我们需要确定AC自动机所需的数据存储结构,它们的用处之后会讲到。

[plain] view plaincopy
  1. struct Node
  2. {
  3. int cnt;//是否为该单词的最后一个结点
  4. Node *fail;//失败指针
  5. Node *next[26];//Trie中每个结点的各个节点
  6. }*queue[500005];//队列,方便用BFS构造失败指针
  7. char s[1000005];//主字符串
  8. char keyword[55];//需要查找的单词
  9. int head,tail;
  10. Node *root;//头结点

第一步:构建Trie

根据输入的 keyword 一 一 构建在Trie树中

[plain] view plaincopy
  1. void Build_trie(char *keyword)//构建Trie树
  2. {
  3. Node *p,*q;
  4. int i,v;
  5. int len=strlen(keyword);
  6. for(i=0,p=root;i<len;i++)
  7. {
  8. v=keyword[i]-'a';
  9. if(p->next[v]==NULL)
  10. {
  11. q=(struct Node *)malloc(sizeof(Node));
  12. Init(q);
  13. p->next[v]=q;//结点链接
  14. }
  15. p=p->next[v];//指针移动到下一个结点
  16. }
  17. p->cnt++;//单词最后一个结点cnt++,代表一个单词
  18. }

构建完成后的效果如下图:

  • 构建失败指针

  • 构建失败指针是AC自动机的关键所在,可以说,若没有失败指针,所谓的AC自动机只不过是Trie树而已。
  • 失败指针原理:
  • 构建失败指针,使当前字符失配时跳转到另一段从root开始每一个字符都与当前已匹配字符段某一个后缀完全相同且长度最大的位置继续匹配,如同KMP算法一样,AC自动机在匹配时如果当前字符串匹配失败,那么利用失配指针进行跳转。由此可知如果跳转,跳转后的串的前缀必为跳转前的模式串的后缀,并且跳转的新位置的深度(匹配字符个数)一定小于跳之前的节点(跳转后匹配字符数不可能大于跳转前,否则无法保证跳转后的序列的前缀与跳转前的序列的后缀匹配)。所以可以利用BFS在Trie上进行失败指针求解。
  • 失败指针利用:
  • 如果当前指针在某一字符s[m+1]处失配,即(p->next[s[m+1]]==NULL),则说明没有单词s[1...m+1]存在,此时,如果当前指针的失配指针指向root,则说明当前序列的任何后缀不是是某个单词的前缀,如果指针的失配指针不指向root,则说明当前序列s[i...m]是某一单词的前缀,于是跳转到当前指针的失配指针,以s[i...m]为前缀继续匹配s[m+1]。
  • 对于已经得到的序列s[1...m],由于s[i...m]可能是某单词的后缀,s[1...j]可能是某单词的前缀,所以s[1...m]中可能会出现单词,但是当前指针的位置是确定的,不能移动,我们就需要temp临时指针,令temp=当前指针,然后依次测试s[1...m],s[i...m]是否是单词。
  • >>>简单来说,失败指针的作用就是将主串某一位之前的所有可以与模式串匹配的单词快速在Trie树中找出。

第二步:构建失败指针

  1. 在构造完Tire树之后,接下去的工作就是构造失败指针。构造失败指针的过程概括起来就一句话:设这个节点上的字母为C,沿着它父亲节点的失败指针走,直到走到一个节点,它的子结点中也有字母为C的节点。然后把当前节点的失败指针指向那个字母也为C的儿子。如果一直走到了root都没找到,那就把失败指针指向root。具体操作起来只需要:先把root加入队列(root的失败指针指向自己或者NULL),这以后我们每处理一个点,就把它的所有儿子加入队列。
  2. 观察构造失败指针的流程:对照图来看,首先root的fail指针指向NULL,然后root入队,进入循环。从队列中弹出root,root节点与s,h节点相连,因为它们是第一层的字符,肯定没有比它层数更小的共同前后缀,所以把这2个节点的失败指针指向root,并且先后进入队列,失败指针的指向对应图中的(1),(2)两条虚线;从队列中先弹出h(右边那个),h所连的只有e结点,所以接下来扫描指针指向e节点的父节点h节点的fail指针指向的节点,也就是root,root->next['e']==NULL,并且root->fail==NULL,说明匹配序列为空,则把节点e的fail指针指向root,对应图中的(3),然后节点e进入队列;从队列中弹出s,s节点与a,h(左边那个)相连,先遍历到a节点,扫描指针指向a节点的父节点s节点的fail指针指向的节点,也就是root,root->next['a']==NULL,并且root->fail==NULL,说明匹配序列为空,则把节点a的fail指针指向root,对应图中的(4),然后节点a进入队列。接着遍历到h节点,扫描指针指向h节点的父节点s节点的fail指针指向的节点,也就是root,root->next['h']!=NULL,所以把节点h的fail指针指向右边那个h,对应图中的(5),然后节点h进入队列...由此类推,最终失配指针如图所示。

构建失败指针的代码:

[plain] view plaincopy
  1. void Build_AC_automation(Node *root)
  2. {
  3. head=0,tail=0;//队列头、尾指针
  4. queue[head++]=root;//先将root入队
  5. while(head!=tail)
  6. {
  7. Node *p=NULL;
  8. Node *temp=queue[tail++];//弹出队头结点
  9. for(int i=0;i<26;i++)
  10. {
  11. if(temp->next[i]!=NULL)//找到实际存在的字符结点
  12. { //temp->next[i] 为该结点,temp为其父结点
  13. if(temp==root)//若是第一层中的字符结点,则把该结点的失败指针指向root
  14. temp->next[i]->fail=root;
  15. else
  16. {
  17. //依次回溯该节点的父节点的失败指针直到某节点的next[i]与该节点相同,
  18. //则把该节点的失败指针指向该next[i]节点;
  19. //若回溯到 root 都没有找到,则该节点的失败指针指向 root
  20. p=temp->fail;//将该结点的父结点的失败指针给p
  21. while(p!=NULL)
  22. {
  23. if(p->next[i]!=NULL)
  24. {
  25. temp->next[i]->fail=p->next[i];
  26. break;
  27. }
  28. p=p->fail;
  29. }
  30. //让该结点的失败指针也指向root
  31. if(p==NULL)
  32. temp->next[i]->fail=root;
  33. }
  34. queue[head++]=temp->next[i];//每处理一个结点,都让该结点的所有孩子依次入队
  35. }
  36. }
  37. }
  38. }
  • 为什么上述那个方法是可行的,是可以保证从root到所跳转的位置的那一段字符串长度小于当前匹配到的字符串长度且与当前匹配到的字符串的某一个后缀完全相同且长度最大呢?

    • 显然我们在构建失败指针的时候都是从当前节点的父节点的失败指针出发,由于Trie树将所有单词中相同前缀压缩在了一起,所以所有失败指针都不可能平级跳转(到达另一个与自己深度相同的节点),因为如果平级跳转,很显然跳转所到达的那个节点肯定不是当前匹配到的字符串的后缀的一部分,否则那两个节点会合为一个,所以跳转只能到达比当前深度小的节点,又因为是由当前节点父节点开始的跳转,所以这样就可以保证从root到所跳转到位置的那一段字符串长度小于当前匹配到的字符串长度。另一方面,我们可以类比KMP求NEXT数组时求最大匹配数量的思想,那种思想在AC自动机中的体现就是当构建失败指针时不断地回到之前的跳转位置,然后判断跳转位置的下一个字符是否包含当前字符,如果是就将失败指针与那个跳转位置连接,如果跳转位置指向NULL就说明当前匹配的字符在当前深度之前没有出现过,无法与任何跳转位置匹配,而若是找到了第一个跳转位置的下一个字符包含当前字符的的跳转位置,则必然取到了最大的长度,这是因为其余的当前正在匹配的字符必然在第一个跳转位置的下一个字符包含当前字符的的跳转位置深度之上,而那样的跳转位置就算可以,也不会是最大的(最后一个字符的深度比当前找到的第一个可行的跳转位置的最后一个字符的深度小,串必然更短一些)。
    • 第三步:匹配
      这样就证明了这种方法构建失败指针的可行性。

第三步:匹配

  1. 最后,我们便可以在AC自动机上查找模式串中出现过哪些单词了。匹配过程分两种情况:(1)当前字符匹配,表示从当前节点沿着树边有一条路径可以到达目标字符,此时只需沿该路径走向下一个节点继续匹配即可,目标字符串指针移向下个字符继续匹配;(2)当前字符不匹配,则去当前节点失败指针所指向的字符继续匹配,匹配过程随着指针指向root结束。重复这2个过程中的任意一个,直到模式串走到结尾为止。
  2. 对例子来说:其中模式串为yasherhs。对于i=0,1。Trie中没有对应的路径,故不做任何操作;i=2,3,4时,指针p走到左下节点e。因为节点e的count信息为1,所以cnt+1,并且讲节点e的count值设置为-1,表示改单词已经出现过了,防止重复计数,最后temp指向e节点的失败指针所指向的节点继续查找,以此类推,最后temp指向root,退出while循环,这个过程中count增加了2。表示找到了2个单词she和he。当i=5时,程序进入第5行,p指向其失败指针的节点,也就是右边那个e节点,随后在第6行指向r节点,r节点的count值为1,从而count+1,循环直到temp指向root为止。最后i=6,7时,找不到任何匹配,匹配过程结束。
  3. AC自动机时间复杂性为:O(L(T)+max(L(Pi))+m)其中m是模式串的数量

匹配代码:

[plain] view plaincopy
  1. int query(Node *root)
  2. { //i为主串指针,p为模式串指针
  3. int i,v,count=0;
  4. Node *p=root;
  5. int len=strlen(s);
  6. for(i=0;i<len;i++)
  7. {
  8. v=s[i]-'a';
  9. //由失败指针回溯查找,判断s[i]是否存在于Trie树中
  10. while(p->next[v]==NULL && p!=root)
  11. p=p->fail;
  12. p=p->next[v];//找到后p指针指向该结点
  13. if(p==NULL)//若指针返回为空,则没有找到与之匹配的字符
  14. p=root;
  15. Node *temp=p;//匹配该结点后,沿其失败指针回溯,判断其它结点是否匹配
  16. while(temp!=root)//匹配结束控制
  17. {
  18. if(temp->cnt>=0)//判断该结点是否被访问
  19. {
  20. count+=temp->cnt;//由于cnt初始化为 0,所以只有cnt>0时才统计了单词的个数
  21. temp->cnt=-1;//标记已访问过
  22. }
  23. else//结点已访问,退出循环
  24. break;
  25. temp=temp->fail;//回溯 失败指针 继续寻找下一个满足条件的结点
  26. }
  27. }
  28. return count;
  29. }
本例题的完整模板代码请点击查看博文:http://blog.csdn.net/liu940204/article/details/51345954
暂时的AC自动机的讲解就这么愉快地结束了,未完待续......

AC自动机算法及模板相关推荐

  1. KMP算法、AC自动机算法的原理介绍以及Python实现

    KMP算法 要弄懂AC自动机算法,首先弄清楚KMP算法. 这篇文章讲的很好: http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E ...

  2. 极限定律 My Algorithm Space AC自动机算法详解

    转载自:http://www.cppblog.com/mythit/archive/2009/04/21/80633.html 首先简要介绍一下AC自动机:Aho-Corasick automatio ...

  3. AC自动机(算法介绍)

    前言: ac自动机能帮你自动通过ac题目(才怪),其实所谓的ac自动机他并不能帮你自动ac,而是一种多模式串的匹配算法.相较于kmp算法在运行多模式串的匹配时只需一次遍历即可,而kmp要针对不同的子序 ...

  4. AC自动机算法详解以及Java代码实现

    详细介绍了AC自动机算法详解以及Java代码实现. 文章目录 1 概念和原理 2 节点定义 3 构建Trie前缀树 3.1 案例演示 4 构建fail失配指针 4.1 案例演示 5 匹配文本 5.1 ...

  5. AC 自动机算法介绍

    一 点睛 AC 自动机是 KMP 算法和 Trie 树的结合,是经典的多模匹配算法.首先将多个模式串构建一棵字典树,然后在字典树上添加失配指针,失配指针相当于 KMP 算法中的 next 函数(匹配失 ...

  6. AC自动机 算法详解(图解)及模板

    要学AC自动机需要自备两个前置技能:KMP和trie树(其实个人感觉不会kmp也行,失配指针的概念并不难) 其中,KMP是用于一对一的字符串匹配,而trie虽然能用于多模式匹配,但是每次匹配失败都需要 ...

  7. 字符串算法 | AC自动机算法

    1.简介 一种多模式串匹配算法, 可以快速从主串中同时找出所有包含的所有模式串. 对比KMP是单模式匹配, 虽然可以使用单模式串匹配算法逐个进行查找模式串, 但是实际场景中,若模式串的数量可能很大,并 ...

  8. AC自动机(题目+模板)

    学习博客:https://www.cnblogs.com/hyfhaha/p/10802604.html AC自动机可以认为是kmp+trie树. trie树数组大小是字符串数目n*最大字符串长度mx ...

  9. Censored! POJ - 1625(AC自动机 + dp +高精度模板)

    题目链接 题目大意:给你一个字母表,给定一些敏感字符串,问长度为m且不含任意敏感字符串的串有多少个.(字符全部来自字母表) 思路:首先第一个坑点是输入的字符是unsigned char,可能出现负的A ...

最新文章

  1. 如何拼通网络ip地址_如何解决IP地址冲突
  2. 童装这门好生意,救得了森马吗?
  3. 【转】独家教程:用PHP编写Android应用程序
  4. java中求立方根_求解立方根
  5. 新手入门python的注意事项_【新手入门Python语言的方法】
  6. 《JavaScript面向对象精要》——1.9 总结
  7. ext 浅谈类的实例
  8. mysql一对多_mysql一对多查询合并多的一方的数据。
  9. X86汇编语言从实模式到保护模式11:指令格式及操作尺寸
  10. Node.js实战(四)之调试Node.js
  11. jdbc executebatch 非事务_面试:Mybatis事务请讲解一下?
  12. android常用窗口动画,android 自定义dialog,窗口动画,
  13. verilog 入门教程
  14. K3 LEDE踩坑专题
  15. 跳跃游戏Ⅱ(C语言)
  16. 【NOIP2016提高A组五校联考1】排队
  17. 白杨SEO:如何用百度好看视频排名优化来做视频营销?
  18. 【微信公众号】7、SpringBoot整合WxJava新增临时、永久素材
  19. matlab 仿真三项异步电机,基于MATLAB三相异步电机的建模与仿真
  20. 互联网快讯:知乎登陆港交所;极米Z6X Pro、极米H3S超强性能获肯定;华为将发布新款折叠屏手机

热门文章

  1. 存在量词后必须用合取式?-数学
  2. 理解和实现分布式TensorFlow集群完整教程
  3. LeetCode简单题之有效的字母异位词
  4. 数据、人工智能和传感器按COVID-19新冠流感排列
  5. h264和h265多维度区别
  6. workerman的基本用法
  7. Android 逐帧动画(Frame)
  8. 文本类控件 (TextView的介绍)
  9. LOJ10074架设电话线
  10. react组件回顶部