AC自动机总结

AC自动机结合了Trie的结构和KMP失配的思想,解决的是多模式串匹配问题。

步骤:

  1. 将所有模式串构成Trie

  2. 对每个Trie上的节点构造失配指针\(fail\)

之后就可以利用构建出的AC自动机解决各种问题。

这里的Trie的节点表示一种状态,状态是某个模式串的前缀(即从Trie上的节点走到根经过的前缀),边表示转移,即转移到在当前节点的状态下加上某个字符后的状态。记所有状态的集合为\(Q\)

\(fail\)的构建

对于状态\(u\)\(fail_u=v\),使得\(v\in Q\)\(v\)\(u\)的最长后缀。

考虑对于当前节点\(u\),设其父亲为\(p\)\(p\)通过字符\(c\)的边指向\(u\)\(tr[p,c]=u\)

我们假设现在深度小于\(u\)的节点的\(fail\)都已经求得。(深度小于\(u\)即状态的长度小于\(u\),这是真正要限定的。)

  • \(tr[fail_p,c]\)存在,那么\(fail_u=tr[fail_p,c]\)。即在\(p\)后加上\(c\)的最长后缀就是在\(fail_p\)后加上\(c\)

  • \(tr[fail_p,c]\)不存在,就继续跳\(fail\)

  • \(fail\)跳到根之后也没找到,那么\(fail_u=0\)

以上只是构建\(fail\)的基本思想。实际运用时,我们要对\(fail\)做路径压缩,即若\(tr[u,c]\)不存在,那么\(tr[u,c]=tr[fail_u,c]\)。这样修改Trie的结构后,我们在跳边的时候可能会丢失前缀,但没关系,后缀能匹配那么原串就一定能匹配,并且\(fail\)其实也是在丢失前缀。

拓扑优化

对于\(fail\)连成的边,有一个性质:在AC自动机中只保留\(fail\)边,一定构成树。这是因为\(fail\)一定不成环,且深度比当前点小。

我们在统计答案时会沿着\(fail\)不断跳,那么原问题就变成了树上的链求和问题,可以拓扑排序优化建图,最后统计答案时按拓扑序递推过去。(不用真建\(fail\)树,只需记录入度。)

实现:

void ins(char s[],int idx){
	int u=0;
	for(int i=0;s[i];++i){
		if(!tr[u][s[i]-'a']) tr[u][s[i]-'a']=++sz;
		u=tr[u][s[i]-'a'];
	}
	if(!val[u]) val[u]=idx;
	rev[idx]=val[u];//或者换成其他操作
}

void build(){
	queue<int> q;
	for(int i=0;i<26;++i){
		if(tr[0][i]) ind[0]++,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],ind[tr[fail[u]][i]]++,q.push(tr[u][i]);
			else tr[u][i]=tr[fail[u]][i];
		}
	}
}

void qry(char s[]){
	int u=0;
	for(int i=0;s[i];++i){
		u=tr[u][s[i]-'a'];
		cnt[u]++;//或者换成其他操作
	}
}

void topu(){
	queue<int> q;
	for(int i=0;i<=sz;++i){
		if(!ind[i]) q.push(i);
	}
	while(!q.empty()){
		int u=q.front();
		q.pop();
		ans[val[u]]=cnt[u];
		int v=fail[u];
		cnt[v]+=cnt[u];//或者换成其他递推操作
		if(!--ind[v]) q.push(v);
	}
}
posted @ 2025-04-18 16:59  RandomShuffle  阅读(28)  评论(0)    收藏  举报