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的博客