AC 自动机
Intention:
又是第不知道多少次被串串题破防的一天,做到最后总是认出我不会的 AC 自动机。所以!写一些我的理解(大部分来源于 OI Wiki),洗刷我被串串题恶心的耻辱。
Introduction:
前置知识:trie.
trie,即字典树,是一种字符前缀树,利用模式串串间重复的前缀,以空间换来极快的查询效率。
这棵树有一些有趣的性质:
- 每条边的边权都是一个字符。
- 从根节点出发到叶节点都是一个被插入的完整的串串。
- 每个节点的连向儿子的边权不会重复。
对于每一串查询的串串,时间复杂度都是
建树:
对于 trie,建树与其他数据结构有些不同。因为节点数量的不固定,需要动态开点。
void ins(char s[])
{
scanf("%s",s+1);
int len=strlen(s+1);
int now=0;
for(int i=1;i<=len;i++)
{
int x=s[i]-'a';
if(!tr[now][x])
tr[now][x]=++trlen;
now=tr[now][x];
}
}
这里的数组含义也与平时图论的存图数组不同,假设有一条从
这样就建好了一棵字典树了。很简单,是吧?
trie 目前只有三种用处:检索字符串、求异或最值、以及 AC 自动机。
正文:
当在一个很长的串串里面检索多个字符串的时候,朴素的做法是从每一个字符开始放到字典树上扫完整个串串。这样显然太慢了。于是 AC 自动机产生了,它可以实现扫一次目标串就可以对所有需要检索的字符串操作。
为了方便讲解,我们约定以下规则:
代表一个已经完成插入模式串的字典树。 代表插入字典树的模式串。- 对于字典树上的每个节点,它代表一个状态
,表示从根节点到该点边上所连成的字符串。显然该字符串为某几个模式串的前缀。 - 对于字典树上的一个节点,它的状态转移来源为节点
。 - 对于字典树的边,边权为
。
当然不止这些规则,还有一些会在下面的定义中提及。
失配指针:
AC 自动机的精髓所在。类似于 KMP 但又有所不同,一个点的
构建方法:
对于一个节点,有
- 如果存在
,那么 指向 。 - 否则找到
(原谅作者突然这么写,不然真的小到看不见了),重复上一步操作。
单这么说可能很难理解,所以上图!
这是一棵插入 hers
、his
、she
、i
的字典树(没错,我把 OI Wiki 的例子搬过来了)。我们针对
- 首先找到
,看是否存在 ,发现 没有边权为 的边,跳到 号节点。 - 再次找到
,看是否存在 ,发现 存在边权为 的边 ,所以节点 的 指向 。
可以发现,
void build()
{
static std::deque<int>q;q.clear();
for(int i=0;i<=26;i++)
if(tr[0][i])
q.push_back(tr[0][i]);
while(q.size())
{
int x=q.front();q.pop_front();
for(int i=0;i<26;i++)
if(tr[x][i])
fail[tr[x][i]]=tr[fail[x]][i],q.push_back(tr[x][i]);
else//难点:!!!!!
tr[x][i]=tr[fail[x]][i];//这里是为了其他节点以该节点作为失配指针时可以直接找到上两行中的 tr[fail[x]][i]。
//本来是要用 while 不断寻找的,这里却用一个仿递归(就是在字典树上做一个路径压缩)的数组存下了最深的对应边
}
}
可以看到,代码中并没有不断的跳
用图可以更好的解释:
可以看到,该图相较于上一张图,多了一条 else
里所做的事。这样,原本两步的操作
该图又新加了一条
查询:
最后这个就很简单了,只要顺着字典树找即可。
void query(char s[])
{
scanf("%s",s+1);
int now=0;
int len=strlen(s+1);
for(int i=1;i<=len;i++)
{
int x=s[i]-'a';
now=tr[now][x];
for(int j=now;j;j=fail[j])
//do something...
}
}
To be better:
但是,仍然有些毒瘤题,会卡 query 中反复跳
观察发现,
于是可以在 build 记录每个节点
void build()
{
static std::deque<int>q;q.clear();
for(int i=0;i<=26;i++)
if(tr[0][i])
q.push_back(tr[0][i]);
while(q.size())
{
int x=q.front();q.pop_front();
for(int i=0;i<26;i++)
if(tr[x][i])
{
fail[tr[x][i]]=tr[fail[x]][i],q.push_back(tr[x][i]);
++indeg[fail[tr[x][i]]];
}
else//难点:!!!!!
tr[x][i]=tr[fail[x]][i];//这里是为了其他节点以该节点作为失配指针时可以直接找到上两行中的 tr[fail[x]][i]。
//本来是要用 while 不断寻找的,这里却用一个仿递归(就是在字典树上做一个路径压缩)的数组存下了最深的对应边
}
}
void query(char s[])
{
scanf("%s",s+1);
int now=0;
int len=strlen(s+1);
for(int i=1;i<=len;i++)
{
int x=s[i]-'a';
now=tr[now][x];
//do something...
}
}
void topu()
{
static std::deque<int>q;q.clear();
for(int i=1;i<=trlen;i++)
if(!indeg[i])
{
q.push_back(i);
indeg[i]=-1;
}
while(q.size())
{
int x=q.front();q.pop_front();
//do something...
--indeg[fail[x]];
if(!indeg[fail[x]])
{
q.push_back(fail[x]);
indeg[fail[x]]=-1;
}
}
}
Code:
最后总代码奉上:
bool _Start;//😅
#define DEBUG
#include<cmath>
#include<deque>
#include<vector>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
namespace IO
{
#define TP template<typename T>
#define TP_ template<typename T,typename ... T_>
#ifdef DEBUG
#define gc() (getchar())
#else
char buf[1<<20],*p1,*p2;
#define gc() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<20,stdin),p1==p2)?EOF:*p1++)
#endif
#ifdef DEBUG
void pc(const char &c)
{
putchar(c);
}
#else
char pbuf[1<<20],*pp=pbuf;
inline void pc(const char &c)
{
if(pp-pbuf==1<<20)
fwrite(pbuf,1,1<<20,stdout),pp=pbuf;
*pp++=c;
}
struct IO{~IO(){fwrite(pbuf,1,pp-pbuf,stdout);}}_;
#endif
TP inline void read(T &x)
{
x=0;static int f;f=0;static char ch;ch=gc();
for(;ch<'0'||ch>'9';ch=gc())ch=='-'&&(f=1);
for(;ch>='0'&&ch<='9';ch=gc())x=(x<<1)+(x<<3)+(ch^48);
f&&(x=-x);
}
TP void write(T x)
{
if(x<0)
pc('-'),x=-x;
static T sta[35],top;top=0;
do
sta[++top]=x%10,x/=10;
while(x);
while(top)
pc(sta[top--]^48);
}
TP_ inline void read(T &x,T_&...y){read(x);read(y...);}
TP void writeln(const T x){write(x);pc('\n');}
TP void writesp(const T x){write(x);pc(' ');}
TP_ void writeln(const T x,const T_ ...y){writesp(x);writeln(y...);}
void writest(const std::string &a){for(int i=0;a[i];i++)pc(a[i]);}
TP inline T max(const T &a,const T &b){return a>b?a:b;}
TP_ inline T max(const T &a,const T_&...b){return max(a,max(b...));}
TP inline T min(const T &a,const T &b){return a<b?a:b;}
TP_ inline T min(const T &a,const T_&...b){return min(a,min(b...));}
TP inline void swap(T &a,T &b){static T t;t=a;a=b;b=t;}
TP inline T abs(const T &a){return a>0?a:-a;}
#undef TP
#undef TP_
}
using namespace IO;
using std::cerr;
using LL=long long;
constexpr int N=2e5+10;
constexpr int S=2e6+10;
namespace Lofty
{
int n;
int tr[N][30];
int fail[N];
int trlen;
int indeg[N];
void ins(char s[],int num)
{
scanf("%s",s+1);
int len=strlen(s+1);
int now=0;
for(int i=1;i<=len;i++)
{
int x=s[i]-'a';
if(!tr[now][x])
tr[now][x]=++trlen;
now=tr[now][x];
}
}
void build()
{
static std::deque<int>q;q.clear();
for(int i=0;i<=26;i++)
if(tr[0][i])
q.push_back(tr[0][i]);
while(q.size())
{
int x=q.front();q.pop_front();
for(int i=0;i<26;i++)
if(tr[x][i])
{
fail[tr[x][i]]=tr[fail[x]][i],q.push_back(tr[x][i]);
++indeg[fail[tr[x][i]]];
}
else//难点:!!!!!
tr[x][i]=tr[fail[x]][i];//这里是为了其他节点以该节点作为失配指针时可以直接找到上两行中的 tr[fail[x]][i]。
//本来是要用 while 不断寻找的,这里却用一个仿递归(就是在字典树上做一个路径压缩)的数组存下了最深的对应边
}
}
void query(char s[])
{
scanf("%s",s+1);
int now=0;
int len=strlen(s+1);
for(int i=1;i<=len;i++)
{
int x=s[i]-'a';
now=tr[now][x];
//do something...
}
}
void topu()
{
static std::deque<int>q;q.clear();
for(int i=1;i<=trlen;i++)
if(!indeg[i])
{
q.push_back(i);
indeg[i]=-1;
}
while(q.size())
{
int x=q.front();q.pop_front();
//do something...
--indeg[fail[x]];
if(!indeg[fail[x]])
{
q.push_back(fail[x]);
indeg[fail[x]]=-1;
}
}
}
char s[N],t[S];
void work()
{
read(n);
for(int i=1;i<=n;i++)
ins(s,i);
build();query(t);topu();
}
}
bool _End;
int main()
{
// fprintf(stderr,"%.2lf MB\n",(&_End-&_Start)/1048576.0);
Lofty::work();
return 0;
}
//😱
后话:
所以呢,串串题还是很恶心。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现