个人总结向博客注意。。。
AC自动机_引入
-
对于k个模式串,我们要匹配一个文本串。
如果采用建立k个next数组的方法(kmp),时间复杂度显然为O((mi+n)*k),不可接受。
-
那我们就需要一种更简便的数据结构(误),来实现在可控时间范围内(O(n))内的匹配。
除此之外,AC自动机还可用于多模式串匹配下的其他算法,如dp等、
AC自动机的构建
AC自动机的框架
- AC自动机的实质是一颗带失配指针的trie树
- fail指针是失配时要跳到的地方,每个节点都有一个fail指针
- 第一层的节点fail指针直接指向0号节点(虚拟节点)
如何建立AC自动机
这里采用构建fail图的方式,会比较好写
- 对于节点a,它在trie树上有3个儿子,b,c,d。(这里节点编号即它的字母)
- 然而它没有儿子e,但是当有另一个节点要访问下一个e时(下文解释),只有沿着fail不断跳到有e为止,还要写一个while
- 于是把这个空节点赋值为它fail指针指向的节点的e节点
- 这样其他节点就可以直接访问这个空节点了
伪代码 1:
if not (tree[now].son[i])
tree[now].son[i]=tree[tree[now].fail].son[i];
对于一个节点的fail值,应该是失配后要跳到的地方。
那么fail所指向的节点应该满足什么条件呢?
- 这个节点在trie上的单词(这个字母及以前)应该包含fail指向的节点的单词(这个字母及以前)
- 由(1)可得,这个字母应该等于fail所指向的字母
所以构建方法就出来了:
对于一个节点,它的父亲 以及 父亲的fail节点一定满足条件 1
所以只用找父亲的 fail的 与自己相同的 儿子就行了QwQ
伪代码 2:
int nex=tree[now].son[i];
tree[nex].fail=tree[tree[now].fail].son[i];
再加上bfs把所有单词遍历完 = 完整代码(建树)
void bfs(){
queue<int>q;
for(int i=0;i<26;i++){
if(tree[0].son[i]) {
tree[tree[0].son[i]].fail=0;
q.push(tree[0].son[i]);
}
} //首先,第一层的fail一定是0;
while(!q.empty()){
int now=q.front();
q.pop();
for(int i=0;i<26;i++){ //注意:所有字母
if(tree[now].son[i]){
int nex=tree[now].son[i];
tree[nex].fail=tree[tree[now].fail].son[i];
q.push(nex); //有儿子
}else tree[now].son[i]=tree[tree[now].fail].son[i]; //没有这个儿子
}
}
}
AC自动机查找
这个就比较简单了。。
既然是 O(n) 的时间,那么肯定要枚举文本串 (逆 因 果 暴 论)
枚举文本串,对于每一位在AC自动机上跳。
如果当前单词已经没有文本串的下一个单词,直接跳到fail所指向的这个单词(有时候要跳很多次)
但是不用。注意到,我们构建的并非fail树,而是fail图。
于是把这个空节点赋值为它fail指针指向的节点的e节点
所以直接用儿子就行了
枚举到每一个点时,沿着fail往上跳。
由于每个结尾点都在trie树的末尾(废话),而且满足
这个节点在trie上的单词(这个字母及以前)应该包含fail指向的节点的单词(这个字母及以前)
的性质,所以沿着fail往上跳,遇到有结尾的标记就++ans(也可以作其他处理)就行了。
下面是喜闻乐见的代码:
void search(){
int now=0,len=strlen(m);
for(int i=0;i<len;i++){ //枚举
now=tree[now].son[m[i]-'a'];
for(int t=now;t;t=tree[t].fail) ans[tree[t].end]++; //如上的操作
}
return;
}
模板代码
模板题: Luogu p3796
我有独特的存字符串手段,不建议学(这都不重要)
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;
int read(){
int x=0,pos=1;char ch=getchar();
for(;!isdigit(ch);ch=getchar()) if(ch=='-') pos=0;
for(;isdigit(ch);ch=getchar()) x=x*10+ch-'0';
return pos?x:-x;
}
int n;char s[1001],m[1000051];
int ans[1001];
char an[201][201];int l[201];
struct ac{
int son[30],fail,end;
}tree[1000001];
int tot=0;
void insert(int cnt){
int now=0;int len=l[cnt];
for(int i=0;i<len;i++){
if(!tree[now].son[s[i]-'a']){
tree[now].son[s[i]-'a']=++tot;
memset(tree[tot].son,0,sizeof(tree[tot].son));
tree[tot].fail=0;
tree[tot].end=0;
}
now=tree[now].son[s[i]-'a'];
}
tree[now].end=cnt;
return;
}
void bfs(){
queue<int>q;
for(int i=0;i<26;i++){
if(tree[0].son[i]) {
tree[tree[0].son[i]].fail=0;
q.push(tree[0].son[i]);
}
}
while(!q.empty()){
int now=q.front();
q.pop();
for(int i=0;i<26;i++){
if(tree[now].son[i]){
int nex=tree[now].son[i];
tree[nex].fail=tree[tree[now].fail].son[i];
q.push(nex);
}else tree[now].son[i]=tree[tree[now].fail].son[i];
}
}
}
void search(){
int now=0,len=strlen(m);
for(int i=0;i<len;i++){
now=tree[now].son[m[i]-'a'];
for(int t=now;t;t=tree[t].fail) ans[tree[t].end]++;
}
return;
}
int main(){
//ios::sync_with_stdio(0);
//cin.tie(0);
while(n=read()){
if(n==0) break;
memset(tree[0].son,0,sizeof(tree[0].son));
tree[0].fail=0;
tree[0].end=0;
memset(ans,0,sizeof(ans));
memset(an,0,sizeof(an));
memset(l,0,sizeof(l));
tot=0;
for(int i=1;i<=n;i++){
scanf("%s",s);
int len=strlen(s);
l[i]=len;
for(int j=0;j<len;j++){
an[i][j]=s[j];
}
insert(i);
}
scanf("%s",m);
bfs();
search();
int maxn=-1;
for(int i=1;i<=n;i++){
if(ans[i]>maxn) maxn=ans[i];
}
printf("%d\n",maxn);
for(int i=1;i<=n;i++){
if(ans[i]==maxn){
for(int j=0;j<l[i];j++){
putchar(an[i][j]);
}
putchar('\n');
}
}
}
return 0;
}
注意事项
- AC自动机是离线数据结构
- 一定要建fail图!!!
其实好像也没几个要注意的(逃
例题
题意:从一个长度不超过10^5的字符串S中删除一些单词,输出最后的S
注意,删除一个单词后可能会导致S中出现另一个列表中的单词!
10^5,一般字符串的题又不带log(当然我粗陋寡闻),只能O(n)
用两个栈模拟即可,主要靠对AC自动机的理解与运用
于是把我给的模板中的search魔改一下就OK QwQ
参考代码:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;
int read(){
int x=0,pos=1;char ch=getchar();
for(;!isdigit(ch);ch=getchar()) if(ch=='-') pos=0;
for(;isdigit(ch);ch=getchar()) x=x*10+ch-'0';
return pos?x:-x;
}
int n;char s[100051],m[100051];
int l[100051];
struct ac{
int son[30],fail,end;
}tree[100011];
int tot=0;
void insert(int cnt){
int now=0;int len=l[cnt];
for(int i=0;i<len;i++){
if(!tree[now].son[s[i]-'a']){
tree[now].son[s[i]-'a']=++tot;
memset(tree[tot].son,0,sizeof(tree[tot].son));
tree[tot].fail=0;
tree[tot].end=0;
}
now=tree[now].son[s[i]-'a'];
}
tree[now].end=len;
return;
}
void bfs(){
queue<int>q;
for(int i=0;i<26;i++){
if(tree[0].son[i]) {
tree[tree[0].son[i]].fail=0;
q.push(tree[0].son[i]);
}
}
while(!q.empty()){
int now=q.front();
q.pop();
for(int i=0;i<26;i++){
if(tree[now].son[i]){
int nex=tree[now].son[i];
tree[nex].fail=tree[tree[now].fail].son[i];
q.push(nex);
}else tree[now].son[i]=tree[tree[now].fail].son[i];
}
}
}
int s1[100010],s2[100051];
void search(){
int now=0,len=strlen(m);
int top=0;
for(int i=0;i<len;i++){
now=tree[now].son[m[i]-'a'];
s1[++top]=now;
s2[top]=i;
if(tree[now].end){
top-=tree[now].end;
now=s1[top];
}
}
for(int i=1;i<=top;i++){
printf("%c",m[s2[i]]);
}
printf("\n");
return;
}
int main(){
scanf("%s",m);
n=read();
tree[0].fail=0;
tree[0].end=0;
for(int i=1;i<=n;i++){
scanf("%s",s);
int len=strlen(s);
l[i]=len;
insert(i);
}
bfs();
search();
return 0;
}
后记
不知不觉就写了5k字了,其实理解了写起来挺快。
建议大家多画画图,特别是多模拟
也可以自己写写博客总结
跑了
upd:更新了更快的,不暴跳fail的做法
2019/5/9
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#include<stack>
using namespace std;
int read(){
int x=0,pos=1;char ch=getchar();
for(;!isdigit(ch);ch=getchar()) if(ch=='-') pos=0;
for(;isdigit(ch);ch=getchar()) x=x*10+ch-'0';
return pos?x:-x;
}
int n;char s[200001],m[2000051];int pans[10000001],siz[10000051];
int ans[200001];
int l[200001],lnk[200001];
struct ac{
int son[30],fail,end;
}tree[10000001];
struct node{
int v,nex;
}edge[1000001];
int tope=0,head[1000001];
void add(int from,int to){
edge[++tope].v=to;
edge[tope].nex=head[from];
head[from]=tope;
}
int tot=0;
void insert(int cnt){
int now=0;int len=l[cnt];
for(int i=0;i<len;i++){
if(!tree[now].son[s[i]-'a']){
tree[now].son[s[i]-'a']=++tot;
memset(tree[tot].son,0,sizeof(tree[tot].son));
}
now=tree[now].son[s[i]-'a'];
}
lnk[cnt]=now;
return;
}
stack<int>st;
void bfs(){
queue<int>q;
for(int i=0;i<26;i++){
if(tree[0].son[i]) {
tree[tree[0].son[i]].fail=0;
q.push(tree[0].son[i]);
}
}
while(!q.empty()){
int now=q.front();
q.pop();
for(int i=0;i<26;i++){
if(tree[now].son[i]){
int nex=tree[now].son[i];
tree[nex].fail=tree[tree[now].fail].son[i];
q.push(nex);
}else tree[now].son[i]=tree[tree[now].fail].son[i];
}
st.push(now);
}
}
void search(){
int now=0,len=strlen(m);
for(int i=0;i<len;i++){
now=tree[now].son[m[i]-'a'];
siz[now]++;
}
now=0;
while(!st.empty()){
now=st.top();st.pop();
siz[tree[now].fail]+=siz[now];
}
return;
}
int main(){
n=read();
tot=0;
for(int i=1;i<=n;i++){
scanf("%s",s);
int len=strlen(s);
l[i]=len;
insert(i);
}
scanf("%s",m);
bfs();
search();
for(int i=1;i<=n;i++){
printf("%d\n",siz[lnk[i]]);
}
return 0;
}