AC自动机模板

AC自动机

Aho-Corasick automaton,该算法在1975年产生于贝尔实验室,是著名的多模匹配算法,又称AC自动机算法。
AC自动机是用来处理字符串匹配问题。KMP是处理单模式串匹配问题,而AC自动机是用于处理多模式串匹配问题,例如:给出 n 个单词,再给出一段包含 m 个字符的文章,问有多少个单词在文章中出现过?以下模板就是根据该问题整理。

使用说明
其中的trie树编号是从 1 开始,每次使用前都要初始化 tot = 1,以及trie数组、End数组都要初始化。
SZ设计要合理,SZ表示字典树中的节点数目上限,太多或太少都会运行错误。
本文利用queue来实现bfs,嫌慢可以手写模拟,不会麻烦多少。

代码示例
Keywords Search

#include<bits/stdc++.h>
using namespace std;

const int N = 1e4+10;	//模式串个数 
const int M = 1e6+10;	//文本串长度 

char str[N][50],s[M];

const int SZ = 50*N;	//trie节点数 
int trie[SZ][30],End[SZ],tot ;
int fail[SZ];			//失配指针,类似于nex
 
inline void init(){
	memset(trie,0,sizeof trie);
	memset(End,0,sizeof End); tot = 1;
}
void Insert(const char* s){
	/* 向trie树中插入字符串s */ 
	int len = strlen(s), p = 1;
	for(int i = 0;i < len;i++){
		int ch = s[i]-'a';
		if(!trie[p][ch]) trie[p][ch] = ++tot;
		p = trie[p][ch];
	}
	End[p]++;
}
queue<int> q;
void getFail(){
	/*利用bfs更新fail数组*/ 
	for(int i = 0;i < 26;i++) trie[0][i] = 1;
	q.push(1); fail[1] = 0;
	while(!q.empty()){
		int p = q.front(); q.pop();
	//	printf("%d\n",q.size());
		for(int i = 0;i < 26;i++){
			if(!trie[p][i]) trie[p][i] = trie[fail[p]][i];
			else{
				q.push(trie[p][i]);
				fail[trie[p][i]] = trie[fail[p]][i];
			} 
		}
	}
}
int ans = 0;
void ask(const char *s){
	/* 统计匹配成功的模式串个数并累加到ans上 */ 
	int len = strlen(s), p = 1;
	for(int i = 0;i < len;i++){
		int ch = s[i]-'a';
		int k = trie[p][ch];
		while(k > 1){
			ans += End[k]; End[k] = 0;
			k = fail[k];
		}
		p = trie[p][ch];
	}
}
int t,n;	//数组组数, 模式串个数 
void solve(){
	for(int i = 1;i <= n;i++) Insert(str[i]);
	getFail(); ans = 0;
	ask(s);
	printf("%d\n",ans);
}
int main(){
	scanf("%d",&t);
	while(t--){
		init();		//多组数据不要忘了初始化! 
		scanf("%d",&n);
		for(int i = 1;i <= n;i++) scanf("%s",str[i]);
		scanf("%s",s);
		solve();
	}
	return 0;
}
对getFail()函数的一些解释

fail 数组就像是 kmp 算法中的 nex 数组一样,是在失配时转移指针用的,利用它可以使得时间控制在O(N)。
就上述代码而言,我们是如何更新fail数组的呢?

  • 首先根节点若失配,则无法匹配字符,即 fail[1] = 0。
  • 若当前位置有字符,则它的失配指针指向“它父亲节点失配指针所指向节点 的 下一个节点”,即 fail[ trie[p][i] ] = fail[ fail[p] ][i],其思想类似kmp中nex转移。
  • 若当前位置无字符,即trie[p][i] = 0,那么就让当前节点指向其失配指针的子节点,即trie[p][i] = trie[ fail[p] ][i]

第 3 步破坏了 trie 的结构,但这样可以优化时间。若不存在trie[p][i] 的转移边则指向 trie[ fail[p] ][i],因为在具体问题中,若不存在trie[p][i] 则需要沿着 p 的前缀指针走到第一个满足存在 i 字符的转移边的点 v,得到trie[v][i],那么我们直接令 trie[p][i] = trie[v][i] 即可使得该情况得到优化,类似记忆化 & 路径压缩。
也正是这个原因,我们在构建 fail 数组时,没有处理 v 的转移边 i 不存在的情况,而是直接fail[ trie[p][i] = trie[v][i](其中 trie[v][i] 在之前已经处理好了)。

参考资料

  • 董永建,信息学竞赛一本通提高版,福州:福建教育出版社,2018.6,93-97
  • bestsort的博客
已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 程序猿惹谁了 设计师:白松林 返回首页