算法总结篇---AC自动机

写在前面

鸣谢:
OiWiki
「笔记」AC 自动机---LuckyBlock
字符串四姐妹---老色批
AC自动机讲解超详细---某不知名大佬html

Q:AC自动机?是能本身AC题目的算法吗?(兴奋)
A:不不不,那叫自动AC机,经过打开答案文件输出答案的一种小手段,在比赛中使用还会有禁赛三年的奖励,而AC自动机是一个字符串匹配算法算法

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

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


算法流程


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

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


引例:

给定 \(n\) 个模式串 \(s_i\) 和一个文本串 \(t\),求有多少个不一样的模式串在文本串里出现过。
两个模式串不一样当且仅当他们编号不一样。指针

概述:

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

  • 一、创建Trie树;htm

  • 二、对Trie树上的全部结点构造失配指针blog

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自动机(二次增强版)

相关文章
相关标签/搜索