AC自动机学习笔记
简介
- AC自动机是一种有限状态自动机,它常被用于多模式串的字符串匹配。
- AC自动机是以TRIE的结构为基础,结合KMP的思想建立的。
构建AC自动机
AC自动机的建立分为两个步骤
\(\quad\)\(\quad\) 1. 将所有的模式串放到一个Trie中。
\(\quad\)\(\quad\) 2. 利用KMP的思想,对所有存在的节点构建失配指针。
Trie的构建
\(\quad\)对于第一个部分,就和Trie的建立一模一样,用insert即可
构造失配指针
\(\quad\)对于第二个部分,他的构造与KMP的Next数组类似。利用已经求过失配指针,来递推新的失配指针,由于所利用的失配指针,其深度都应当小于当前点,所以可以考虑利用bfs。
我们跳转到父亲的fail指针指向的结点fail[p];
\(\quad\) 1.如果结点fail[p]通过字母c连接到的子结点w存在:
\(\quad\)\(\quad\)则让u的fail指针指向这个结点fail[u]=w)。相当于在p和fail[p]后面加一个字符c,就构成了fail[u]。
\(\quad\) 2.如果fail[p]通过字母c连接到的子结点w不存在:
\(\quad\)\(\quad\)那么我们继续找到fail[fail[p]]指针指向的结点,重复上述判断过程,一直跳fail指针直到根节点。如果真的没有,就令fail[u]=根节点。
下面贴上这部分的代码
1.insert
点击查看代码
void build(string s){
int l=s.size();
int P=0;
for(int i=0;i<l;i++){
if(AC[P].vis[s[i]-'a']==0){
AC[P].vis[s[i]-'a']=++cnt;
clean(cnt);
}
P=AC[P].vis[s[i]-'a'];
}
AC[P].shu++;
}
2.失配指针
点击查看代码
void getfail(){
queue<int> q;
for(int i=0;i<26;i++){
if(AC[0].vis[i]!=0){
AC[AC[0].vis[i]].fail=0;
q.push(AC[0].vis[i]);
}
}
while(!q.empty()){
int u=q.front();
q.pop();
for(int i=0;i<26;i++){
if(AC[u].vis[i]!=0){
AC[AC[u].vis[i]].fail=AC[AC[u].fail].vis[i];
q.push(AC[u].vis[i]);
}
else
AC[u].vis[i]=AC[AC[u].fail].vis[i];
}
}
}
多模式匹配
点击查看代码
int query(string s){
int l=s.size();
int p=0,ans=0;
for(int i=0;i<l;i++){
p=AC[p].vis[s[i]-'a'];
for(int t=p;t&&AC[t].shu!=-1;t=AC[t].fail){
ans+=AC[t].shu;
AC[t].shu=-1;
}
}
return ans;
}
p就是当前匹配到的节点,而ans就是答案。循环遍历匹配串,p在字典树上寻找当前字符。利用fail指针找出所有匹配的模式串,累加到答案中,然后清0。对e[j]取反的操作用来判断e[j]是否等于-1。
总结
构建失配指针实现多模式匹配。
例题&代码
T1 Luogu P3808
板子题
Luogu P3808
#include<bits/stdc++.h>
using namespace std;
int n;
string s;
const int N=1e6+5;
struct Tree{
int fail;
int vis[26];
int shu;
}AC[N];
int cnt=0;
void build(string s){
int l=s.size();
int P=0;
for(int i=0;i<l;i++){
if(AC[P].vis[s[i]-'a']==0){
AC[P].vis[s[i]-'a']=++cnt;
clean(cnt);
}
P=AC[P].vis[s[i]-'a'];
}
AC[P].shu++;
}
void getfail(){
queue<int> q;
for(int i=0;i<26;i++){
if(AC[0].vis[i]!=0){
AC[AC[0].vis[i]].fail=0;
q.push(AC[0].vis[i]);
}
}
while(!q.empty()){
int u=q.front();
q.pop();
for(int i=0;i<26;i++){
if(AC[u].vis[i]!=0){
AC[AC[u].vis[i]].fail=AC[AC[u].fail].vis[i];
q.push(AC[u].vis[i]);
}
else
AC[u].vis[i]=AC[AC[u].fail].vis[i];
}
}
}
int query(string s){
int l=s.size();
int p=0,ans=0;
for(int i=0;i<l;i++){
p=AC[p].vis[s[i]-'a'];
for(int t=p;t;t=AC[t].fail){
ans[AC[t].shu].num++;
}
}
return ans;
}
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>s;
build(s);
}
AC[0].fail=0;
getfail();
cin>>s;
cout<<query(s)<<"\n";
}
T2 Oj P427 [HDU 2222]Keywords Search
还是板子题
Keywords Search
#include<bits/stdc++.h>
using namespace std;
int n;
string s;
const int N=1e6+5;
struct Tree{
int fail;
int vis[26];
int shu;
}AC[N];
int cnt=0;
void build(string s){
int l=s.size();
int P=0;
for(int i=0;i<l;i++){
if(AC[P].vis[s[i]-'a']==0)
AC[P].vis[s[i]-'a']=++cnt;
P=AC[P].vis[s[i]-'a'];
}
AC[P].shu++;
}
void getfail(){
queue<int> q;
for(int i=0;i<26;i++){
if(AC[0].vis[i]!=0){
AC[AC[0].vis[i]].fail=0;
q.push(AC[0].vis[i]);
}
}
while(!q.empty()){
int u=q.front();
q.pop();
for(int i=0;i<26;i++){
if(AC[u].vis[i]!=0){
AC[AC[u].vis[i]].fail=AC[AC[u].fail].vis[i];
q.push(AC[u].vis[i]);
}
else
AC[u].vis[i]=AC[AC[u].fail].vis[i];
}
}
}
int query(string s){
int l=s.size();
int p=0,ans=0;
for(int i=0;i<l;i++){
p=AC[p].vis[s[i]-'a'];
for(int t=p;t&&AC[t].shu!=-1;t=AC[t].fail){
ans+=AC[t].shu;
AC[t].shu=-1;
}
}
return ans;
}
int main(){
int T;
cin>>T;
while(T--){
cin>>n;
memset(AC,0,sizeof(AC));
cnt=0;
for(int i=1;i<=n;i++){
cin>>s;
build(s);
}
AC[0].fail=0;
getfail();
cin>>s;
cout<<query(s)<<"\n";
}
}
T3 Luogu P3796 AC 自动机(简单版 II)
就是T1改了一下输入输出
Luogu P3796
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
struct Tree{
int vis[26];
int fail,end;
}ac[N];
int cnt=0;
struct Node{
int num,pos;
}ans[N];
bool cmp(Node a,Node b){
if(a.num!=b.num)
return a.num>b.num;
return a.pos<b.pos;
}
string s[N],t;
void clean(int x){
for(int i=0;i<26;i++)
ac[x].vis[i]=0;
ac[x].fail=ac[x].end=0;
}
void build(string s,int num){
int l=s.size(),p=0;
for(int i=0;i<l;i++){
if(ac[p].vis[s[i]-'a']==0){
ac[p].vis[s[i]-'a']=++cnt;
clean(cnt);
}
p=ac[p].vis[s[i]-'a'];
}
ac[p].end=num;
}
void getfail(){
queue<int>q;
for(int i=0;i<26;i++){
if(ac[0].vis[i]!=0){
ac[ac[0].vis[i]].fail=0;
q.push(ac[0].vis[i]);
}
}
while(!q.empty()){
int u=q.front();
q.pop();
for(int i=0;i<26;i++){
if(ac[u].vis[i]!=0){
ac[ac[u].vis[i]].fail=ac[ac[u].fail].vis[i];
q.push(ac[u].vis[i]);
}
else
ac[u].vis[i]=ac[ac[u].fail].vis[i];
}
}
}
void query(string s){
int l=s.size(),p=0,anss=0;
for(int i=0;i<l;i++){
p=ac[p].vis[s[i]-'a'];
for(int tt=p;tt;tt=ac[tt].fail)
ans[ac[tt].end].num++;
}
}
int main(){
int n;
while(cin>>n){
if(n==0)
break;
cnt=0;
clean(0);
for(int i=1;i<=n;i++){
cin>>s[i];
ans[i].num=0;
ans[i].pos=i;
build(s[i],i);
}
ac[0].fail=0;
getfail();
cin>>t;
query(t);
sort(ans+1,ans+n+1,cmp);
cout<<ans[1].num<<"\n";
cout<<s[ans[1].pos]<<"\n";
for(int i=2;i<=n;i++){
if(ans[i].num==ans[i-1].num)
cout<<s[ans[i].pos]<<"\n";
else
break;
}
}
return 0;
}
T4 [JSOI2012] 玄武密码
这个题就是先跑一遍AC自动机,然后将文本串匹配好后,再跑出前缀,再将模式串跑一遍Trie树,就得到了最长公共前缀长度了。
玄武密码
#include<bits/stdc++.h>
using namespace std;
int n,m;
const int N=1e7+5;
const int M=1e5+5;
string s[M],rr;
struct Tree{
int fail;
int vis[4];
int end;
}AC[N];
int cnt=0,anss[M],viss[N];
int get(char ch){
if(ch=='E')
return 0;
else if(ch=='S')
return 1;
else if(ch=='W')
return 2;
else
return 3;
}
void build(string s){
int l=s.size();
int P=0;
for(int i=0;i<l;i++){
if(AC[P].vis[get(s[i])]==0)
AC[P].vis[get(s[i])]=++cnt;
P=AC[P].vis[get(s[i])];
}
AC[P].end++;
}
void getfail(){
queue<int> q;
for(int i=0;i<4;i++){
if(AC[0].vis[i]!=0){
AC[AC[0].vis[i]].fail=0;
q.push(AC[0].vis[i]);
}
}
while(!q.empty()){
int u=q.front();
q.pop();
for(int i=0;i<4;i++){
if(AC[u].vis[i]!=0){
AC[AC[u].vis[i]].fail=AC[AC[u].fail].vis[i];
q.push(AC[u].vis[i]);
}
else
AC[u].vis[i]=AC[AC[u].fail].vis[i];
}
}
int p=0;
for(int i=0;i<m;i++){
p=AC[p].vis[get(rr[i])];
for(int k=p;k&&!viss[k];k=AC[k].fail)
viss[k]=1;
}
}
int query(string s){
int l=s.size();
int p=0,res=0;
for(int i=0;i<l;i++){
p=AC[p].vis[get(s[i])];
if(viss[p])
res=i+1;
}
return res;
}
int main(){
int T;
T=1;
while(T--){
cin>>m>>n;
cin>>rr;
memset(AC,0,sizeof(AC));
cnt=0;
for(int i=1;i<=n;i++){
cin>>s[i];
build(s[i]);
}
AC[0].fail=0;
getfail();
for(int i=1;i<=n;i++){
cout<<query(s[i])<<"\n";
}
}
}
T5 Luogu P5357 AC自动机二次加强版
再暴力跳fail的时候存在大量的重复更新,造成时间复杂度的大量增加,我们可以采用拓扑排序的方式进行更新,可以直接越过一些节点,直接更新更深一层。(没太理解明白
Luogu P5357
#include<bits/stdc++.h>
using namespace std;
const int N=2e6+5;
char s[N],t[N];
int n,cnt,vis[200051],ans,in[N],mp[N];
struct Tree{
int vis[26];
int fail,zhi;
int flag;
}ac[N];
queue<int> q;
void insert(char* s,int num){
int u=1,l=strlen(s);
for(int i=0;i<l;i++){
int v=s[i]-'a';
if(!ac[u].vis[v])
ac[u].vis[v]=++cnt;
u=ac[u].vis[v];
}
if(!ac[u].flag)
ac[u].flag=num;
mp[num]=ac[u].flag;
}
void getfail(){
for(int i=0;i<26;i++)
ac[0].vis[i]=1;
q.push(1);
while(!q.empty()){
int u=q.front();
q.pop();
for(int i=0;i<26;i++){
int v=ac[u].vis[i];
if(!v){
ac[u].vis[i]=ac[ac[u].fail].vis[i];
continue;
}
ac[v].fail=ac[ac[u].fail].vis[i];
in[ac[v].fail]++;
q.push(v);
}
}
}
void topu(){
for(int i=1;i<=cnt;i++)
if(in[i]==0)
q.push(i);
while(!q.empty()){
int u=q.front();q.pop();
vis[ac[u].flag]=ac[u].zhi;
int v=ac[u].fail;
in[v]--;
ac[v].zhi+=ac[u].zhi;
if(in[v]==0)
q.push(v);
}
}
void query(char* s){
int u=1,len=strlen(s);
for(int i=0;i<len;i++){
u=ac[u].vis[s[i]-'a'];
ac[u].zhi++;
}
}
int main(){
cin>>n;
cnt=1;
for(int i=1;i<=n;i++){
cin>>s;
insert(s,i);
}
getfail();
cin>>t;
query(t);
topu();
for(int i=1;i<=n;i++)
cout<<vis[mp[i]]<<"\n";
}
T6[TJOI2013]单词
这题题目其实和上一题差不多,但是这个题卡了我\(1h\)多,这里建议大家再也不要用\(stl\)里的\(strlen\),直接给我打了\(string\)倍的常数,非常的恶心,最好是转成\(string\),用\(string.size()\)会更快,因为\(size()\)是\(O(1)\),真的很快。下面贴上代码
[TJOI2013]单词
#include<iostream>
#include<queue>
#include<cstring>
#define il inline
#define re register
using namespace std;
const int N=2e6+5;
char t[N];
string s;
int n,cnt,vis[200051],ans,in[N],mp[N];
struct Tree{
int vis[26];
int fail,zhi;
int flag;
}ac[N];
queue<int> q;
il void insert(string,int num){
int u=1,l=s.size();
for(re int i=0;i<l;++i){
int v=s[i]-'a';
if(!ac[u].vis[v])
ac[u].vis[v]=++cnt;
u=ac[u].vis[v];
}
if(!ac[u].flag)
ac[u].flag=num;
mp[num]=ac[u].flag;
}
il void getfail(){
for(re int i(0);i<26;++i)
ac[0].vis[i]=1;
q.push(1);
while(!q.empty()){
int u=q.front();
q.pop();
for(re int i(0);i<26;++i){
int v=ac[u].vis[i];
if(!v){
ac[u].vis[i]=ac[ac[u].fail].vis[i];
continue;
}
ac[v].fail=ac[ac[u].fail].vis[i];
in[ac[v].fail]++;
q.push(v);
}
}
}
il void topu(){
for(re int i(1);i<=cnt;++i)
if(in[i]==0)
q.push(i);
while(!q.empty()){
int u=q.front();q.pop();
vis[ac[u].flag]=ac[u].zhi;
int v=ac[u].fail;
in[v]--;
ac[v].zhi+=ac[u].zhi;
if(in[v]==0)
q.push(v);
}
}
il void query(char* s){
int u=1,len=strlen(s);
for(re int i(0);i<len;++i){
if(s[i]=='z'+1){
u=1;
continue;
}
u=ac[u].vis[s[i]-'a'];
ac[u].zhi++;
}
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n;
cnt=1;
int zong=-1;
for(re int i(1);i<=n;++i){
cin>>s;
for(re int j(0);j<s.size();++j)
t[++zong]=s[j];
t[++zong]='z'+1;
insert(s,i);
}
getfail();
query(t);
topu();
for(re int i(1);i<=n;++i)
cout<<vis[mp[i]]<<"\n";
}
Update
\(\quad\)\(2024.2.8:\)做题时发现CF710F这道题需要在线的\(AC自动机\),所以便学习了一下这种应用,假设现在我们有m个字符串,我们可以构建多个\(AC自动机\)。每一个都包含\(2^k\)个字符串,每新添加一个字符串,我们便和前面的合并。过程如下:
8 4 2 1 +1
8 4 2 1 1 -> 8 4 2 2 -> 8 4 4 -> 8 8 -> 16
这便是合并的过程,于是便支持在线查询了。(稍后贴上代码

浙公网安备 33010602011771号