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";
}

还是板子题

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
这便是合并的过程,于是便支持在线查询了。(稍后贴上代码

posted @ 2025-02-06 16:34  Dengyouk  阅读(35)  评论(0)    收藏  举报