强势无图详解AC自动机

@


个人总结向博客注意。。。

AC自动机_引入

  • 对于k个模式串,我们要匹配一个文本串。

    如果采用建立k个next数组的方法(kmp),时间复杂度显然为O((mi+n)*k),不可接受。

  • 那我们就需要一种更简便的数据结构(误),来实现在可控时间范围内(O(n))内的匹配。
    除此之外,AC自动机还可用于多模式串匹配下的其他算法,如dp等、

AC自动机的构建

AC自动机的框架

  1. AC自动机的实质是一颗带失配指针的trie树
  2. fail指针是失配时要跳到的地方,每个节点都有一个fail指针
  3. 第一层的节点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所指向的节点应该满足什么条件呢?

  1. 这个节点在trie上的单词(这个字母及以前)应该包含fail指向的节点的单词(这个字母及以前)
  2. 由(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;
} 

注意事项

  1. AC自动机是离线数据结构
  2. 一定要建fail图!!!

其实好像也没几个要注意的(逃

例题

点我QAQ

题意:从一个长度不超过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;
} 
posted @ 2019-07-27 22:34  lcyfrog  阅读(159)  评论(0编辑  收藏  举报