zoukankan      html  css  js  c++  java
  • 算法总结篇---AC自动机

    写在前面

    鸣谢:
    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自动机(二次加强版)

  • 相关阅读:
    google PR值突然调整,貌似出什么问题了
    【转自译言】在线劝说:7种说服人们网络购买的方法
    马化腾关于产品设计与用户体验的培训
    北京站售票人员倒票视频
    大型网站架构不得不考虑的10个问题
    在谈电子商务名词解释
    GridView
    CheckBoxList
    ShoppingCart
    MongoDB数据库简介及安装
  • 原文地址:https://www.cnblogs.com/Silymtics/p/14255674.html
Copyright © 2011-2022 走看看