AC 自动机学习笔记
AC 自动机学习笔记
AC 自动机可以用于解决字符串上的出现次数,出现位置问题。结合了 Trie 树和 KMP 的思想,在
算法
前置知识:Trie 树,KMP,自动机基本概念。
构建 AC 自动机有两个步骤:
1.基础 Trie 结构:将所有模式串构建成一颗 Trie 树。
2.KMP 思想:对 Trie 树上的节点构造失配指针。
然后进行多模式匹配。
构建 Trie 树
直接把所有模式串构建普通的 Trie 树,同时强调,Trie 树中的某个节点表示了某个模式串的前缀。
后文将前缀也成为状态。
失配指针
AC 自动机利用 fail 指针来优化多模式串的匹配。
状态
如果不存在最长后缀,那么 fail 会指向 Trie 树的根节点。
匹配时,文本串会从根开始沿着和当前字符一样的边向下,如果在某一个节点无法向下,我们称为失配。
在文本串失配时,将会沿着当前节点的 fail 指针前往下一个点继续匹配。
(而且你会发现,文本串到达了某个状态,那么沿着这个状态的 fail 指针遍历,这些遍历的状态也都在文本串中出现过,这个性质后面会用到。)
证明求出 fail 指针呢?
设
1.如果
2.否则重复使
3.真的不存在,让 fail 指针指向根。
如此完成 fail 的构建。
字典树与字典图
1.字典树:
void insert(char *s,int num)
{
int u=1,len=strlen(s+1);
for(int i=1;i<=len;i++)
{
int v=s[i]-'a';
if(!trie[u].son[v]) trie[u].son[v]=++cnt;
u=trie[u].son[v];
}
if(!trie[u].flg) trie[u].flg=num;
rev[num]=trie[u].flg;
}
这里的 flg 指向了到达这个节点的第一个模式串,如果有别的模式串相同,那么我们让这个模式串指向第一个相同的即可。
2.字典图
我们用文本串遍历字典树时,如果失配要沿着 fail 指针找到一个可以匹配的节点,但这个过程往往会耗费很多时间。
考虑能不能优化这个过程,我们设
特别的,如果根节点的
不难发现,
如果
这样子我们就省去了遍历 fail 链的时间,直接通过
void getfail()
{
for(int i=0;i<26;i++) trie[0].son[i]=1;//tr[0,c] 均指向根
que.push(1);
trie[1].fail=0;//根的 fail 指针指向 0,方便后面求 tr[u,c]
while(!que.empty())
{
int u=que.front();
que.pop();
int fail=trie[u].fail;
for(int i=0;i<26;i++)
{
int v=trie[u].son[i];
if(!v)
{
trie[u].son[i]=trie[fail].son[i];
continue;
}
trie[v].fail=trie[fail].son[i];
que.push(v);
}
}
}
多模式匹配
朴实无华的做法:
设当前的字文本串为
一开始
沿着
这里之前分析过,如果该状态出现过,那么该状态的最长后缀也出现过。为了避免漏掉状态,这也是我们 fail 指针指向最长后缀。
最后输出结束位置的出现次数即可。
华丽的做法:
式证明 fail 链将会形成树。
显然,由于 fail 指针指向比自己深度小的节点,而且不可能有环。得证。
由于每个节点都可以通过 fail 链回到根,那我们可以基于朴实无华的做法分出两种做法。
1.拓扑排序优化
遍历字典树时,只在当前节点出现次数
最后,通过在 fail 树上拓扑排序求出答案。
建图这么写(记录入度即可):
void getfail()
{
for(int i=0;i<26;i++) trie[0].son[i]=1;
que.push(1);
trie[1].fail=0;
while(!que.empty())
{
int u=que.front();
que.pop();
int fail=trie[u].fail;
for(int i=0;i<26;i++)
{
int v=trie[u].son[i];
if(!v)
{
trie[u].son[i]=trie[fail].son[i];
continue;
}
trie[v].fail=trie[fail].son[i];
indeg[trie[fail].son[i]]++;//记录入度
que.push(v);
}
}
}
拓扑排序加查询这么写:
void query(char *s)
{
int u=1,len=strlen(s+1);
for(int i=1;i<=len;i++)
u=trie[u].son[s[i]-'a'],trie[u].ans++;
}
void topu()
{
for(int i=1;i<=cnt;i++) if(!indeg[i]) que.push(i);
while(!que.empty())
{
int u=que.front();
que.pop();
vis[trie[u].flg]=trie[u].ans;
int v=trie[u].fail;
trie[v].ans+=trie[u].ans;
if(!(--indeg[v])) que.push(v);
}
}
然后合起来:
#include<bits/stdc++.h>
using namespace std;
const int maxn=8e5+5,maxm=2e6+5;
int n,cnt=1,ans;
int vis[maxn],rev[maxn],indeg[maxn];
char s[maxm];
struct node
{
int son[27];
int fail,ans,flg;
void clr(){memset(son,0,sizeof(son));fail=flg=0;}
}trie[maxn];
void insert(char *s,int num)
{
int u=1,len=strlen(s+1);
for(int i=1;i<=len;i++)
{
int v=s[i]-'a';
if(!trie[u].son[v]) trie[u].son[v]=++cnt;
u=trie[u].son[v];
}
if(!trie[u].flg) trie[u].flg=num;
rev[num]=trie[u].flg;
}
queue<int>que;
void getfail()
{
for(int i=0;i<26;i++) trie[0].son[i]=1;
que.push(1);
trie[1].fail=0;
while(!que.empty())
{
int u=que.front();
que.pop();
int fail=trie[u].fail;
for(int i=0;i<26;i++)
{
int v=trie[u].son[i];
if(!v)
{
trie[u].son[i]=trie[fail].son[i];
continue;
}
trie[v].fail=trie[fail].son[i];
indeg[trie[fail].son[i]]++;
que.push(v);
}
}
}
void query(char *s)
{
int u=1,len=strlen(s+1);
for(int i=1;i<=len;i++)
u=trie[u].son[s[i]-'a'],trie[u].ans++;
}
void topu()
{
for(int i=1;i<=cnt;i++) if(!indeg[i]) que.push(i);
while(!que.empty())
{
int u=que.front();
que.pop();
vis[trie[u].flg]=trie[u].ans;
int v=trie[u].fail;
trie[v].ans+=trie[u].ans;
if(!(--indeg[v])) que.push(v);
}
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%s",s+1),insert(s,i);
getfail();
scanf("%s",s+1);
query(s);
topu();
for(int i=1;i<=n;i++) printf("%d\n",vis[rev[i]]);
}
子树求和
通过上述查询方法,在最后每个节点用 fail 树上信息求和即可。
例题
挖个坑。
推荐阅读
通过 gif 的方式,写的很好。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具