[模板] AC自动机
AC 自动机
AC 自动机 (Aho-Corasick Automation) 是一个多模式字符串匹配算法.
定义 \(fail\) 函数为该状态 (节点) 代表的字符串的最长后缀所在的状态. 特别的, 如果不存在这样的状态, \(fail\) 函数设为根.
为了提高构建和匹配的效率, 在建立 trie 树之后, 定义
\[child(p, c) = \begin{cases} v & (p 存在 c 的转移) \\ child(fail(p), c) & (不断跳 fail 函数到第一个有 c 的转移的状态) \\ root & (不存在这样的状态) \end{cases}
\]
也可以把这种定义了新的 \(child\) 转移的 AC 自动机称为 trie 图.
这样的话, 设 \(N\) 为 trie 树的节点数, \(\Sigma\) 为字符集大小, 模式串的长度和为 \(T\), 文本串的长度为 \(S\),
构建的时间复杂度为 \(O(N \cdot \Sigma)\), 空间复杂度为 \(O(N \cdot \Sigma)\);
匹配的时间复杂度为 \(O(S)\).
另一种实现是:
不补全不存在的转移, 并且利用 unsigned int
压位维护 \(\Sigma\) 个转移是否存在, 求 \(fail\) 函数 / 匹配的过程直接跳 \(fail\) 函数.
可以利用势能分析证明, 这样的时间复杂度为 \(O(T)\).
代码
const int ndsz=1e6+50;
struct tac{
int ch[ndsz][30],fi[ndsz],end[ndsz],pn=0,rt=0;
int tr(int v){return v-'a'+1;}
int newnd(){return ++pn;}
void clear(){
memset(ch,0,sizeof(ch));
memset(fi,0,sizeof(fi));
memset(end,0,sizeof(end));
pn=0,rt=0;
}
void insert(char *s){
int p=rt,v;
for(int i=0;s[i];++i){
v=tr(s[i]);
if(ch[p][v]==0)ch[p][v]=newnd();
p=ch[p][v];
}
++end[p];
}
void build(){
static int qu[ndsz],qh=1,qt=0;
rep(i,1,26)if(ch[rt][i])fi[ch[rt][i]]=0,qu[++qt]=ch[rt][i];
while(qh<=qt){
int u=qu[qh++];
rep(i,1,26){
if(ch[u][i]==0)ch[u][i]=ch[fi[u]][i];
else fi[ch[u][i]]=ch[fi[u]][i],qu[++qt]=ch[u][i];
}
}
}
int match(char *s){
int now=rt,ans=0;
for(int i=0;s[i];++i){
now=ch[now][tr(s[i])];
for(int p=now;p&&~end[p];p=fi[p])ans+=end[p],end[p]=-1; //每个模式串只匹配一次
}
return ans;
}
void pr(){
printf("pn=%d,rt=%d\n",pn,rt);
rep(i,0,pn){
printf("fi=%d,end=%d,",fi[i],end[i]);
rep(j,1,26)printf("%d ",ch[i][j]);
printf("\n");
}
}
}ac;
附加的应用
DP
由于 AC 自动机属于 DAG , 可以在自动机上进行动态规划.
last指针
有时题目所求仅与关键点 (模式串终点) 有关. 可以定义 \(last(p)\) 表示 \(p\) 跳 fail 指针到达的第一个关键点.
\(last(p)\) 容易通过动态规划求出.
fail 树
将所有fail指针反向, 可以得到一棵以原 trie 树的根为根的树, 可以称之为 fail 树.
fail树有如下的性质:
- 每个节点代表某个模式串的一个前缀;
- 节点的父亲代表它的满足 1. 的最长后缀.
我们还可以得到这样的一些结论:
- 一个节点的子树大小即为它在所有模式串中出现的次数.
- ...