AC自动机

AC自动机

AC自动机适用于多个字符串的匹配,以及一系列引申出来的问题

Trie图

通过将Trie树转化为Trie图,并且使这些非树边到达的地方是“所有模式串的前缀中匹配当前状态的最长后缀。”

上图!(灰色的边:字典树的边,黑色的边:AC 自动机修改字典树结构连出的边。)

​ 例如,当前的状态是\(hers\),而下一个字母是\(h\),而在原本的Trie树上是没有边的,也就是失配了,而在Trie图上,我们转移到了\(hersh\)的在“模式串的前缀中匹配当前状态的最长后缀”,即\(sh\),从而能尽可能的匹配到模式串

fail数组

和Trie的非树边类似,fail数组指向当前状态\(S\)的“所有模式串的前缀中匹配当前状态的最长后缀。”

同样是上面的图(黄色的边:fail 指针)

fail\([she]\)便指向\(he\)

fail的指向还会构成一颗树,满足任意一节点\(S\)的祖先节点全都是\(S\)的后缀

构建

按Trie树的深度(字符串的长度)从小到大处理(bfs)

设当前状态是\(sh\),我们从\(a\sim z\)枚举,如果有在Trie树的边(\(tr[sh][e]\)),就更新\(fail[tr[sh][e]]\),否则则修改非树边\(tr[sh][i]\)

那用什么更新呢?;

以下用\(x\)表示当前节点,\(son\)表示下一个节点,\(son=x+s\),即当前边表示\(s\)

想一下fail的定义,“所有模式串的前缀中匹配当前状态的最长后缀。”,一个一个去找肯定不可能,那能不能利用已经求来的呢?

答案是肯定的,而且就是运用\(x\)的信息,\(fail[x]\)是匹配\(x\)的最长后缀,如果\(tr[fail[x]][s]\)是原本Trie树上的边,那\(fail[son]=tr[fail[x]][s]\)

那如果不是呢,也没有关系,此时\(tr[fail[x]][s]\)就表示匹配\(fail[x]\)的最长后缀,也就是匹配\(x\)的最长后缀,也就是\(fail[son]=tr[fail[x]][s]\)

同样,更新非树边也是这个道理

代码:

void add(string a){
	m=a.size();
	int p=0;
	for(int i=0;i<m;i++){
		if(tr[p][a[i]-'a'])p=tr[p][a[i]-'a'];
		else{
			top++;
			tr[p][a[i]-'a']=top;
			p=top;
		}
	}
	en[p]++;
}
void build(){
	queue<int> q;
	for(int i=0;i<=25;i++){
		if(tr[0][i])q.push(tr[0][i]);
	}
	while(q.size()){
		int x=q.front();q.pop();
		for(int i=0;i<=25;i++){
			if(tr[x][i]){
				fail[tr[x][i]]=tr[fail[x]][i];
				q.push(tr[x][i]);
			}
			else tr[x][i]=tr[fail[x]][i];
		}
	}
}

运用

  • 基础板子
  • DP
  • fail树

总结

AC自动机实际上就是对Trie树进行一些处理,使来处理的匹配串能在失配的情况下,迅速跳到最能匹配的位置,即“所有模式串的前缀中匹配当前状态的最长后缀。”

同时,fail数组则可以维护当前状态的最长的为Trie节点的后缀,通过fail树的操作还可以处理所有为Trie节点的后缀

posted @ 2022-02-17 16:01  qwq_123  阅读(35)  评论(0编辑  收藏  举报