一.含义
AC自动机,著名的多模式串匹配算法之一。
AC自动机就是在Trie树上实现KMP算法
二.步骤
1.建树:将所有模式串构建成一棵Trie树
void add(char *s){
int u=1,len=strlen(s);
for(int i=0;i<len;i++){
int c=s[i]-'a';
if(!ch[u][c]){
ch[u][c]=++cnt;
}
u=ch[u][c];
}
++ex[u];
return ;
}
2.预处理失配指针nxt
void bfs(){//构建nxt指针,补全所有边转移
for(int i=0;i<26;i++)
ch[0][i]=1;
que[1]=1;nxt[1]=0;
for(int q1=1,q2=1;q1<=q2;q1++){
int u=que[q1];
for(int i=0;i<26;i++){
if(!ch[u][i]){
ch[u][i]=ch[nxt[u]][i];
}
else {
que[++q2]=ch[u][i];
int v=nxt[u];
nxt[ch[u][i]]=ch[v][i];
}
}
}
return ;
}
三.例题(模板)
题目:单词查询
题目描述
给定n个长度不超过50的由小写英文字母组成的单词用于查询,以及一篇长为m的文章,问:文中出现了多少个待查询的单词。
输入格式
第一行一个整数T,表示数据组数;
对于每组数据,第一行一个整数n,接下去n行表示n个单词,最后一行输入一个字符串,表示文章。
输出格式
对于每组数据,输出一个数,表示文中出现了多少个待查询的单词。
样例
输入样例
1
5
she
he
say
shr
her
yasherhs
输出样例
3
代码实现:(详解)
#include<bits/stdc++.h>
// 包含了 C++ 标准库中的所有头文件,不推荐在实际编程中使用,可能会导致编译时间增加和潜在的命名冲突
using namespace std;
// 使用 std 命名空间,使得在后续代码中可以直接使用其中的函数和对象,而无需加上 std:: 前缀
const int M=1e6+10;
// 定义一个常量 M,表示一个较大的整数
char s[M];
// 定义一个字符数组 s,用于存储输入的字符串
int ch[M][27],nxt[M],que[M],ex[M],ans,cnt=1;
// 定义多个整数数组 ch、nxt、que、ex,以及整数变量 ans 和 cnt,并初始化 cnt 为 1
void add(char *s){
// 定义一个名为 add 的函数,用于向 AC 自动机中添加字符串
int u=1,len=strlen(s);
// 初始化一个节点索引 u 为 1,并获取输入字符串的长度
for(int i=0;i<len;i++){
// 遍历输入字符串
int c=s[i]-'a';
// 获取当前字符对应的索引
if(!ch[u][c]){
// 如果当前节点没有对应字符的子节点
ch[u][c]=++cnt;
// 创建新的子节点,并更新节点计数
memset(ch[cnt],0,sizeof(ch[cnt]));
// 清空新节点的子节点信息
}
u=ch[u][c];
// 移动到当前字符对应的子节点
}
++ex[u];
// 标记当前节点为一个模式字符串的结束节点
return ;
// 函数返回
}
void bfs(){
// 定义一个名为 bfs 的函数,用于构建 AC 自动机的 nxt 指针
for(int i=0;i<26;i++)
ch[0][i]=1;
// 将根节点的所有子节点初始化为 1
que[1]=1;nxt[1]=0;
// 初始化队列,并设置根节点的 nxt 指针为 0
for(int q1=1,q2=1;q1<=q2;q1++){
// 进行广度优先搜索
int u=que[q1];
// 获取当前队列中的节点
for(int i=0;i<26;i++){
// 遍历所有可能的字符
if(!ch[u][i]){
// 如果当前节点没有该字符的子节点
ch[u][i]=ch[nxt[u]][i];
// 设置为父节点 nxt 指针对应位置的子节点
}
else {
// 如果有子节点
que[++q2]=ch[u][i];
// 将子节点加入队列
int v=nxt[u];
// 获取父节点的 nxt 指针指向的节点
nxt[ch[u][i]]=ch[v][i];
// 设置子节点的 nxt 指针
}
}
}
return ;
// 函数返回
}
void find(char *s){
// 定义一个名为 find 的函数,用于在 AC 自动机中查找输入字符串
int u=1,len=strlen(s),k;
// 初始化节点索引、字符串长度和临时变量
for(int i=0;i<len;i++){
// 遍历输入字符串
int c=s[i]-'a';
// 获取当前字符的索引
k=ch[u][c];
// 获取当前字符对应的子节点
while(k>1){
// 当子节点不是根节点
ans+=ex[k];
// 累加结束节点的计数
ex[k]=0;
// 清空结束节点的计数
k=nxt[k];
// 移动到 nxt 指针指向的节点
}
u=ch[u][c];
// 移动到当前字符对应的子节点
}
return ;
// 函数返回
}
int main(){
// 主函数,程序的入口点
int t,n;
// 定义两个整数变量 t 和 n
cin>>t;
// 从输入读取 t 的值
while(t--){
// 进行 t 次操作
cin>>n;
// 读取每次操作中模式字符串的数量
ans=0,cnt=1;
// 初始化结果变量和节点计数
memset(ex,0,sizeof(ex));
// 清空结束节点的计数数组
for(int i=0;i<26;i++){
ch[0][i]=1;
ch[1][i]=0;
}
// 初始化根节点和第一层节点的信息
while(n--){
cin>>s;
add(s);
}
// 读取 n 个模式字符串并添加到自动机中
bfs();
// 构建 nxt 指针
cin>>s;
// 读取要查找的字符串
find(s);
// 在自动机中查找
cout<<ans<<endl;
// 输出查找结果
}
return 0;
// 程序正常结束返回 0
}