AC自动机
模版题:
洛谷3808【模板】AC 自动机(简单版) 裸的AC自动机
洛谷3796【模板】AC 自动机(加强版) 同上,只需要修改统计答案的部分
洛谷5357【模板】AC 自动机(二次加强版) 需要拓扑排序优化
参考博客:
https://oi-wiki.org/string/ac-automaton/
https://www.cnblogs.com/cjyyb/p/7196308.html
在学AC自动机之前,先学好Trie和kmp。
AC自动机用于解决多模式串匹配的问题,建立在Trie的基础上。
先考虑在Trie树上暴力进行多模式串匹配:对于文本串的每一个字符,都在Trie树上从头开始寻找一次,复杂度是O(文本串长度*Trie树深度)。
现在换个角度思考,对于文本串的每一个字符,不考虑以它开头开始遍历,而是考虑以它结尾的模式串有哪些。首先找到一个以它结尾的最长的Trie树上从根开始的链(即找到从根开始的最长的链满足它是文本串当前前缀的后缀),接着直接跳转到另一条次长的链满足上述条件。跳转至的位置就是fail指针(失配指针)所指的节点。
如何求fail?
根(节点0)的fail=0,根的所有子节点的fail=0。
对于其他的节点,它的子节点的fail是它的fail的对应子节点。具体来说:对于任意一个节点u,若trie[u].to[i]存在,则trie[u].to[i]的fail为trie[trie[u].fail].to[i],即节点的fail指针指向的节点的相应子节点;若trie[u].to[i]不存在,则给trie[u].to[i]赋trie[trie[u].fail].to[i](和上面一样)。
bfs按照上述式子求即可。
拓扑排序优化AC自动机:
- problem:
在之前的AC自动机中,每次匹配时,对于每一个位置,都需要一直跳fail直到根,这一步很费时 - hint:
由于每一个节点都只有一个fail指针,而且一定指向的是比自己深度浅的节点,所以只保留fail指针的话,会形成一棵树,换言之,无环。
这样一来,每次跳fail相当于在树上往上移动一个点->AC自动机的匹配转换成在fail树上的链求和问题。对于当前的点,需要对fail树上它的祖先节点都贡献一个答案。 - solution:
我们并不需要真的建立一棵fail树,只需要记录每个节点的入度(用于拓扑排序)
在匹配时,先将答案累积到当前节点,然后进行拓扑排序即可
关于end的Tips:
trie树的end可以根据需要赋值,如出现的次数,出现的字符串的编号,当前的节点编号等等
在洛谷模版简单版中,记录的是该串出现的次数,即有多少个模版串是这个串;在洛谷模板加强版中,记录的是串的编号;在洛谷模板二次加强版中,记录的Trie树结尾节点的编号(在这题中其实可以不记录end)。
具体赋值的选择,是为最后统计答案服务的。
另外,注意看清题目说明,模式串是否互不相同。
洛谷3808【模板】AC 自动机(简单版)
//Luogu3808 【模板】AC自动机 https://www.luogu.com.cn/problem/P3808
//给定若干个模式串和一个文本串,求有多少种模式串在文本串中出现过,模式串不同当且仅当它们的编号不同
//By DTTTTTTT
//2023/10/25
#include<iostream>
#include<cstdio>
#include<queue>
using namespace std;
const int N=1e6+5;
int n,tot,ans;
string s[N],t;
struct node{
int to[30];
int end;
int fail;
}trie[N];
//构建trie树
void build_trie(){
for(int j=1;j<=n;++j){ //将s[j]插入trie树
int cur=0;//根节点编号为0
int len_j=s[j].length();
for(int i=0;i<len_j;++i){
int c=s[j][i]-'a';
if(!trie[cur].to[c]) trie[cur].to[c]=++tot;
cur=trie[cur].to[c];
}
++trie[cur].end;
}
}
//求失配指针
void build_fail(){
queue<int>q;
//单独处理根结点的孩子的fail
for(int i=0;i<26;++i){
if(trie[0].to[i]){
trie[trie[0].to[i]].fail=0; //指向根结点
q.push(trie[0].to[i]);
}
}
//bfs求出所有的fail【失配指针】
/*
对于任意一个节点u,若trie[u].to[i]存在,则trie[u].to[i]的fail为trie[trie[u].fail].to[i],
即节点的fail指针指向的节点的相应子节点。
若trie[u].to[i]不存在,则给trie[u].to[i]赋trie[trie[u].fail].to[i](和上面一样)。
*/
while(!q.empty()){
int u=q.front();
q.pop();
for(int i=0;i<26;++i){ //枚举所有子节点
if(trie[u].to[i]){
trie[trie[u].to[i]].fail=trie[trie[u].fail].to[i];
q.push(trie[u].to[i]);
}
else
trie[u].to[i]=trie[trie[u].fail].to[i];
}
}
}
//匹配求解答案
void ac_solve(){
int len_t=t.length();
int cur=0;
for(int i=0;i<len_t;++i){
int c=t[i]-'a';
cur=trie[cur].to[c];
for(int t=cur;t && trie[t].end!=-1;t=trie[t].fail){
ans+=trie[t].end;
trie[t].end=-1; //标记计算过了
}
}
}
int main(){
ios_base::sync_with_stdio(false),cin.tie(0),cout.tie(0);
//input
cin>>n;
for(int i=1;i<=n;++i) cin>>s[i];
cin>>t;
//build trie
build_trie();
//build fail
build_fail();
//求解答案:文本串中出现了多少个不同的模式串
//注意这里求的是出现的不同模式串的种类数,不是出现次数,即同一个模式串最多对答案贡献一次
ac_solve();
cout<<ans<<endl;
return 0;
}
洛谷3796【模板】AC 自动机(加强版)
//Luogu3796 【模板】AC 自动机(加强版)https://www.luogu.com.cn/problem/P3796
//给定若干个模式串和文本串,输出在文本串中出现次数最多的模式串,按输入顺序排列(保证没有相同的模式串)
//By DTTTTTTT
//2023/10/25
#include<iostream>
#include<queue>
#include<algorithm>
using namespace std;
const int N=1e6+5;
int n,tot;
string s[N],t;
struct node{
int to[30];
int fail,end;
//trie树的end可以根据需要赋值,如出现的次数,出现的字符串的编号,当前的节点编号等等
//在洛谷模版简单版中,记录的是该串出现的次数,即有多少个模版串是这个串
//在此题(洛谷模板加强版)中,记录的是串的编号
//具体赋值的选择,是为最后统计答案服务的
}ac[N];
struct node1{
int id;
int num;
}ans[N];
void clean(int x){ //初始化ac[x]
for(int i=0;i<26;++i)
ac[x].to[i]=0;
ac[x].fail=ac[x].end=0;
}
void build_trie(){
for(int j=1;j<=n;++j){
int len_j=s[j].length();
int cur=0;
for(int i=0;i<len_j;++i){
int c=s[j][i]-'a';
if(!ac[cur].to[c]) ac[cur].to[c]=++tot,clean(tot);
cur=ac[cur].to[c];
}
ac[cur].end=j; //记录这个字符串的id
}
}
void build_fail(){
queue<int> q;
ac[0].fail=0;
for(int i=0;i<26;++i){
int son=ac[0].to[i];
if(son){
ac[son].fail=0;
q.push(son);
}
}
while(!q.empty()){
int u=q.front();
q.pop();
for(int i=0;i<26;++i){
if(ac[u].to[i]){
ac[ac[u].to[i]].fail=ac[ac[u].fail].to[i];
q.push(ac[u].to[i]);
}
else
ac[u].to[i]=ac[ac[u].fail].to[i];
}
}
}
void ac_solve(){
int cur=0;
int len_t=t.length();
for(int i=0;i<len_t;++i){
int c=t[i]-'a';
cur=ac[cur].to[c];
for(int t=cur;t;t=ac[t].fail)
if(ac[t].end)
++ans[ac[t].end].num;
}
}
bool cmp(node1 x,node1 y){
if(x.num==y.num) return x.id<y.id;
return x.num>y.num;
}
int main(){
ios_base::sync_with_stdio(false),cin.tie(0),cout.tie(0);
while(1){
cin>>n;
if(n==0) break;
tot=0;
clean(0);
for(int i=1;i<=n;++i) {
cin>>s[i];
ans[i].id=i;
ans[i].num=0;
}
cin>>t;
build_trie();
build_fail();
ac_solve();
sort(ans+1,ans+n+1,cmp);
cout<<ans[1].num<<endl<<s[ans[1].id]<<endl;
for(int i=2;i<=n;++i)
if(ans[i].num==ans[1].num)
cout<<s[ans[i].id]<<endl;
else
break;
}
return 0;
}
/*
此题有多组输入,需要每次clear
这里每次对需要使用的节点才clear(学到了!)
具体来说:
在每组测试例子的开头,设置tot=0,并且clean(0)
在构建trie树的过程中,若ac[cur].to[c]没有,需要对其赋值++tot时,clean(tot)
*/
洛谷5357【模板】AC 自动机(二次加强版)
//Luogu5357 【模板】AC 自动机(二次加强版)https://www.luogu.com.cn/problem/P5357
//给定若干个模式串和文本串,求出每个模式串在文本串中出现的次数(数据不保证任意两个模式串不相同)
//n <= 2*10^5 模式串的长度总和不超过2*10^5 文本串的长度不超过2*10^6
//【拓扑排序优化AC自动机】
/*
思想:
problem:
在之前的AC自动机中,每次匹配时,对于每一个位置,都需要一直跳fail直到根,这一步很费时
hint:
由于每一个节点都只有一个fail指针,而且一定指向的是比自己深度浅的节点,所以只保留fail指针的话,会形成一棵树,换言之,无环。
这样一来,每次跳fail相当于在树上往上移动一个点->AC自动机的匹配转换成在fail树上的链求和问题
对于当前的点,需要对fail树上它的祖先节点都贡献一个答案
solution:
我们并不需要真的建立一棵fail树,只需要记录每个节点的入度
在匹配是,先将答案累积到当前节点
然后进行拓扑排序即可
*/
//By DTTTTTTT
//2023/10/25
#include<iostream>
#include<queue>
using namespace std;
const int N=2e5+5;
int n,tot,in_deg[N],pos[N];
string s[N],t;
struct node{
int to[30];
int end,fail;
int ans;
}ac[N];
void build_trie(){
for(int j=1;j<=n;++j){
int len_j=s[j].length();
int cur=0;
for(int i=0;i<len_j;++i){
int c=s[j][i]-'a';
if(!ac[cur].to[c]) ac[cur].to[c]=++tot;
cur=ac[cur].to[c];
}
ac[cur].end=cur; //其实在这一题中甚至不需要记录end
pos[j]=cur; //记录s[j]的最后一个字符在trie树上的节点
/*
注意这么写是错的:
ac[cur].end=tot;
pos[j]=tot; //记录s[j]的最后一个字符在trie树上的tot节点
因为在重复出现同一个模式串的前提下,tot并不代表当前结尾的节点编号
*/
}
}
void build_fail(){
queue<int>q;
ac[0].fail=0;
for(int i=0;i<26;++i)
if(ac[0].to[i]){
ac[ac[0].to[i]].fail=0;
q.push(ac[0].to[i]);
}
while(!q.empty()){
int u=q.front();
q.pop();
for(int i=0;i<26;++i){
if(ac[u].to[i]) {
ac[ac[u].to[i]].fail=ac[ac[u].fail].to[i];
++in_deg[ac[ac[u].fail].to[i]]; //增加入度
q.push(ac[u].to[i]);
}
else ac[u].to[i]=ac[ac[u].fail].to[i];
}
}
}
void ac_solve(){
int len_t=t.length();
int cur=0;
for(int i=0;i<len_t;++i){
int c=t[i]-'a';
cur=ac[cur].to[c];
++ac[cur].ans;
}
}
void topu(){
queue<int>q;
for(int i=1;i<=tot;++i)
if(!in_deg[i]) q.push(i);
while(!q.empty()){
int u=q.front();
q.pop();
int fa=ac[u].fail;
--in_deg[fa];
ac[fa].ans+=ac[u].ans;
if(in_deg[fa]==0) q.push(fa);
}
}
int main(){
ios_base::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n;
for(int i=1;i<=n;++i) cin>>s[i];
cin>>t;
build_trie();
build_fail();
ac_solve();
topu();
for(int i=1;i<=n;++i)
cout<<ac[pos[i]].ans<<endl;
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现