$\text{AC}$ 自动机
\(\text{AC}\) 自动机
\(\text{Part I}\) 何为 \(\text{AC}\) 自动机
\(\text{AC}\) 自动机能够解决多个字符串 \(s_i\) 在某个字符串 \(t\) 匹配的问题
其算法过程如下:
-
把所有的 \(s_i\) 建立一棵 \(trie\)
-
求出每个点的 \(fail\) 编号
-
把 \(t\) 在 \(trie\) 树中搞匹配
总而言之,\(\text{AC}\) 自动机的核心在于建 \(trie\) 加求出 \(fail\) 的过程
\(fail\) 指针的定义,求法以及作用
\(fail\) 的定义
\(fail_i\) 的定义是:与以 \(i\) 节点为结尾的串的后缀有最大公共长度的前缀的结尾编号
画个图更好理解
比如说我们已经插入了字符串 \(\text{FG , HERS , HIS , SHE}\) 到 \(trie\) 树中,那么 \(trie\) 就长这样
其中实线箭头表示字符串中的字符,虚线箭头表示某个点 \(i\) 连向 \(fail_i\)
就例如字符串 \(\text{SHE}\) 中的 \(\text E\) 的 \(fail\) 是 \(\text{HERS}\) 中的 \(\text E\),因为 \(\text{SHE}\) 中以 \(\text E\) 结尾的后缀 \(\text{HE}\) 在 \(\text{HERS}\) 中出现过,且容易发现 \(\text{HE}\) 已经是最长的
如果点 \(i\) 没有这样的匹配的话那么 \(i\) 的 \(fail\) 就为 \(0\) (根)
\(fail\) 的求法
首先我们可以确定,每一个点 \(i\) 的 \(fail\) 指针指向的点的深度一定是比 \(i\) 小的(\(fail\) 是后缀 \(=\) 前缀,前缀必须从头开始匹配)
第一层的 \(fail\) 一定指的是根
点 \(i\) 的父亲 \(fa\) 的 \(fail\) 指针指的是 \(fail_{fa}\),那么如果 \(fail_{fa}\) 有和 \(i\) 值相同的儿子 \(j\),那么 \(i\) 的 \(fail\) 就指向 \(j\)
由于我们在处理 \(i\) 的情况必须要先处理好 \(fa\) 的情况,也就是说 \(trie\) 树中的所有节点的 \(fail\) 必须按深度从小到大求,所以求 \(fail\) 我们使用 \(bfs\) 来实现
实现的一些细节:
-
如果不存在一个节点 \(i\),那么我们可以将那个节点设为\(fail_{fa}\)的值和 \(i\) 相同的儿子,方便跳 \(fail\)
-
无论\(fail_{fa}\)存不存在和 \(i\) 值相同的儿子 \(j\),我们都可以将 \(i\) 的 \(fail\) 指向 \(j\) (因为在处理 \(i\) 的时候 \(j\) 已经处理好了,如果出现这种情况,\(j\) 的值是第 \(1\) 种情况,也是有实际值的)
inline void get_fail(){
queue <int> q;
for(int i=0;i<26;++i){
int x=c[0][i];
fail[x]=0;//第一层节点fail指向根
if(x) q.push(x);
}
while(!q.empty()){
int x=q.front();
q.pop();
for(int i=0;i<26;++i){
if(c[x][i]){
fail[c[x][i]]=c[fail[x]][i];//细节2
q.push(c[x][i]);
}
else
c[x][i]=c[fail[x]][i];//细节1
}
}
}
\(fail\) 的运用
类似于 \(\text{KMP}\) 的 \(next\) 数组,\(fail\) 指针的一大运用就是可以通过让 \(t\) 不断在 \(trie\) 树上跳 \(fail\) 指针从而找到匹配
inline int query(int n,char *A){
int rt=0,res=0;
for(int i=0;i<n;++i){
rt=c[rt][A[i]-'a'];
for(int tmp=rt;tmp&&vis[tmp]!=-1;tmp=fail[tmp]){
res+=vis[tmp];
vis[tmp]=-1;
}
}
return res;
}
当然,这只是 \(fail\) 指针最为基础的运用,后面还会有许多 \(fail\) 的妙用
P3808 【模板】AC 自动机(简单版)
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+5;
int cnt;
int c[N][26],vis[N];
int fail[N];
inline void insert(int n,char *A){
int rt=0;
for(int i=0;i<n;++i){
if(!c[rt][A[i]-'a'])
c[rt][A[i]-'a']=++cnt;
rt=c[rt][A[i]-'a'];
}
++vis[rt];
}
inline void get_fail(){
queue <int> q;
for(int i=0;i<26;++i){
int x=c[0][i];
if(x) q.push(x);
}
while(!q.empty()){
int x=q.front();
q.pop();
for(int i=0;i<26;++i){
if(c[x][i]){
fail[c[x][i]]=c[fail[x]][i];
q.push(c[x][i]);
}
else
c[x][i]=c[fail[x]][i];
}
}
}
inline int query(int n,char *A){
int rt=0,res=0;
for(int i=0;i<n;++i){
rt=c[rt][A[i]-'a'];
for(int tmp=rt;tmp&&vis[tmp]!=-1;tmp=fail[tmp]){
res+=vis[tmp];
vis[tmp]=-1;
}
}
return res;
}
int n;
char A[N];
signed main(){
cin>>n;
while(n--){
scanf("%s",A);
insert(strlen(A),A);
}
get_fail();
scanf("%s",A);
cout<<query(strlen(A),A);
}
\(\text{Part II}\) \(fail\) 指针的其他用法
其实 \(fail\) 指针的用法不止于此
因为 \(fail\) 能从某个匹配的地方跳到另一个,所以就有了在建好 \(fail\) 后 \(trie\) 树上的 \(dp\) 题
习题
P3041 Video Game G
我们先把所有的串插到 \(trie\) 树里面并跑出 \(fail\) 指针
然后考虑 \(trie\) 树中每个点的价值
显然 \(trie\) 中每个点的价值等于以它本身结尾的字符串个数加上以它的所有可以跳到的 \(fail\) 结尾的字符串个数
这个东西可以在算 \(fail\) 的时候顺带计算出来
我们设 \(trie\) 上点 \(i\) 的贡献为 \(v_i\),点 \(i\) 的子节点为 \(ch_{1\sim 3}\)
然后想想如何 \(dp\)
我们设 \(f_{i,j}\) 表示已经记录了前 \(i\) 个字符,且当前节点在 \(trie\) 树中编号为 \(j\) 的最大得分
那么就可以得到:
code:
#include<bits/stdc++.h>
using namespace std;
const int N=1005;
inline int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return x*f;
}
struct trie{
int c[3];
int v,f;
}t[N*5];
int cnt;
inline void cmax(int &a,int b){if(a<b) a=b;}
namespace AC{
inline void insert(int n,char *A){
int rt=0;
for(int i=0;i<n;++i){
int ch=A[i]-'A';
if(!t[rt].c[ch]) t[rt].c[ch]=++cnt;
rt=t[rt].c[ch];
}
++t[rt].v;
}
inline void get_fail(){
queue <int> q;
for(int i=0;i<3;++i)
if(t[0].c[i]){
q.push(t[0].c[i]);
t[0].f=0;
}
while(!q.empty()){
int x=q.front();
q.pop();
for(int i=0;i<3;++i){
int y=t[x].c[i];
if(y){
t[y].f=t[t[x].f].c[i];
q.push(y);
}
else t[x].c[i]=t[t[x].f].c[i];
}
t[x].v+=t[t[x].f].v;
}
}
}
int n,k,ans;
char s[N];
int f[N][305];
signed main(){
n=read(),k=read();
for(int i=1;i<=n;++i){
scanf("%s",s);
AC::insert(strlen(s),s);
}
AC::get_fail();
for(int i=0;i<=k;++i)
for(int j=1;j<=cnt;++j)
f[i][j]=-114514;
for(int i=1;i<=k;++i)
for(int j=0;j<=cnt;++j)
for(int ch=0;ch<3;++ch)
cmax(f[i][t[j].c[ch]],f[i-1][j]+t[t[j].c[ch]].v);
for(int i=0;i<=cnt;++i) cmax(ans,f[k][i]);
printf("%d",ans);
}