zoukankan      html  css  js  c++  java
  • AC自动机——多个kmp匹配

    (并不能自动AC)

    介绍:

    Aho-Corasick automaton,最经典的处理多个模式串的匹配问题。

    是kmp和字典树的结合。

     

    精髓与灵魂:

    ①利用trie处理多个模式串

    ②引入fail指针。节点x的fail表示,trie中最大的某个前缀等于x到根节点字符串后缀的节点位置。

    fail类比于kmp的nxt数组,可以在失配的时候,O(1)找到最大的可能能继续匹配的位置。

    所以,ac自动机可看做多个kmp

     

     

    步骤:(完整代码在下面)

    ①建trie树。插入模式串。

    void ins(char *s){
            int len=strlen(s+1);
            int now=0;
            for(int i=1;i<=len;i++){
                int x=s[i]-'A';
                if(!a[now][x]) a[now][x]=++cnt;
                now=a[now][x];
            }
            exi[now]=1;
        }

    ②trie上建ac自动机。

    void build(){
            queue<int>q;
            for(int i=0;i<26;i++){
                if(a[0][i]) fail[a[0][i]]=0,q.push(a[0][i]);
            }
            while(!q.empty()){
                int x=q.front();q.pop();
                exi[x]|=exi[fail[x]];
                for(int i=0;i<26;i++){
                    if(a[x][i]){
                        fail[a[x][i]]=a[fail[x]][i];
                        q.push(a[x][i]);
                    }
                    else{
                        a[x][i]=a[fail[x]][i];    
                    }
                }
            }
        }

    用bfs来建造,并且,即时转移fail指针。

    fail指针的转移正确性:

    因为bfs是分层加入元素,而fail至少让字符串长度-1,所以之前的fail[x]的各种信息都处理完毕了。

    并且,由于fail的定义,所以能在fail[x][i]往下转移,一定就是最优的。

     

    这里有个小优化:

    else{a[x][i]=a[fail[x]][i];}

    如果x没有i这个儿子,那么就直接指向它fail指针位置的儿子。

    这样子,在之后的if(a[x][i])中,可以直接走一次fail[x]的儿子就可以找到真正的fail[a[x][i]]了。

    因为,如果fail没有这个儿子,不加这个优化还要继续跳fail,复杂度没有保证了。

    这样,就类似于并查集的路径压缩思想,直接指到最长的有这个儿子的点了。

    (语言表达不好,自行画图理解吧。。。)

     

    完整代码:

    struct node{
    	int a[N*N][26],cnt;
    	int fail[N*N];
    	bool exi[N*N];
    	void init(){
    		memset(a,0,sizeof a);memset(exi,0,sizeof exi);
    		cnt=0;memset(fail,0,sizeof fail);
    	}
    	void ins(char *s){
    		int len=strlen(s+1);
    		int now=0;
    		for(int i=1;i<=len;i++){
    			int x=s[i]-'A';
    			if(!a[now][x]) a[now][x]=++cnt;
    			now=a[now][x];
    		}
    		exi[now]=1;
    	}
    	void build(){
    		queue<int>q;
    		for(int i=0;i<26;i++){
    			if(a[0][i]) fail[a[0][i]]=0,q.push(a[0][i]);
    		}
    		while(!q.empty()){
    			int x=q.front();q.pop();
    			exi[x]|=exi[fail[x]];
    			for(int i=0;i<26;i++){
    				if(a[x][i]){
    					fail[a[x][i]]=a[fail[x]][i];
    					q.push(a[x][i]);
    				}
    				else{
    					a[x][i]=a[fail[x]][i];	
    				}
    			}
    		}
    	}
    }ac;
    

    另外,我们ac自动机上节点上,也可以加上其他的标记。

     

    例题:

    [JSOI2007]文本生成器

    Description:

    给n个模式串,求有多少个长度为m的文章,至少包含一个模式串

    Solution:

    Ac自动机的标志很明显,多个模式串,一个主串。

    Ac自动机dp的状态很套路,一般就是匹配到j位置,怎么怎么样。。

    设f[i][j],前i个字符,匹配到AC自动机的j位置,没出现一个模式串的方案数。(最后总方案-没出现一个方案,差分)

    每个点有一个exi布尔数组,表示匹配到这个点,这个点所代表的前缀(可以是一个完整模式串)是否已经包含了至少一个模式串。

    插入的时候,末尾exi=1,bfs的时候,exi[x]|=exi[fail[x]]即可。正确性同bfs分层图性质。

    然后判断,直接转移就好了。

    代码:

    #include<bits/stdc++.h>
    using namespace std;
    const int N=100+3;
    const int mod=10007;
    struct node{
        int a[N*N][26],cnt;
        int fail[N*N];
        bool exi[N*N];
        void init(){
            memset(a,0,sizeof a);memset(exi,0,sizeof exi);
            cnt=0;memset(fail,0,sizeof fail);
        }
        void ins(char *s){
            int len=strlen(s+1);
            int now=0;
            for(int i=1;i<=len;i++){
                int x=s[i]-'A';
                if(!a[now][x]) a[now][x]=++cnt;
                now=a[now][x];
            }
            exi[now]=1;
        }
        void build(){
            queue<int>q;
            for(int i=0;i<26;i++){
                if(a[0][i]) fail[a[0][i]]=0,q.push(a[0][i]);
            }
            while(!q.empty()){
                int x=q.front();q.pop();
                exi[x]|=exi[fail[x]];
                for(int i=0;i<26;i++){
                    if(a[x][i]){
                        fail[a[x][i]]=a[fail[x]][i];
                        q.push(a[x][i]);
                    }
                    else{
                        a[x][i]=a[fail[x]][i];    
                    }
                }
            }
        }
    }ac;
    int n,m;
    int f[N][N*N];
    char s[N];
    int main()
    {
        scanf("%d%d",&n,&m);
        for(int i=1;i<=n;i++){
            scanf("%s",s+1);
            ac.ins(s);
        }
        ac.build();
        int tot=ac.cnt;
        f[0][0]=1;
        for(int i=0;i<=m-1;i++){
            for(int j=0;j<=tot;j++){
                if(!ac.exi[j]){
                    for(int k=0;k<26;k++){
                        if(!ac.exi[ac.a[j][k]]){
                            f[i+1][ac.a[j][k]]=(f[i+1][ac.a[j][k]]+f[i][j])%mod;
                        }
                    }
                }
            }
        }
        long long ans=1;
        for(int i=1;i<=m;i++){
            ans=(ans*26)%mod;
        }
        for(int i=0;i<=tot;i++){
            if(!ac.exi[i]){
                ans=(ans-f[m][i]+mod)%mod;
            }
        }
        printf("%lld",ans);
        return 0;
    }

    为什么要记录AC自动机上匹配到的状态呢?

    因为,单纯记录这一位是哪个字符肯定不能判断是否包含。

    而AC自动机本身的fail,就蕴含了所有的可能包含的位置。只要不断跳fail即可。

    相当于该状态已经包罗万象。

    因为fail的定义,也不会包含更多,不会包含更少。

    所以对于匹配问题再适合不过了。

    AC自动机dp出题人出烦了之后,

    就开始出一些涉及AC自动机形态的题目,更贴近算法本身。

    基本开刀处都是fail指针(AC自动机的精髓嘛)

    [POI2000]病毒

    这是一个利用fail指针构可能可以和trie树构成环的特性。从而构造出无限长的串。

    [NOI2011]阿狸的打字机——AC自动机之fail树的利用

    这个题目恰好相反,把fail树和trie树都利用起来,并且离线处理。

    这两个题目都是值得思考总结的。都利用了fail的性质,但是都没有直接使用fail指针。妙哉!

  • 相关阅读:
    OO开发
    重构:Extract Method (提炼函数)
    Vue 一个组件引用另一个组件
    闭包
    视差滚动原理与实现
    JS中的constructor 和 prototype
    jQuery 为动态添加的元素绑定事件
    setTimeout 的理解
    关于Vertical Align的理解
    网页特效:滚动视差设计指南
  • 原文地址:https://www.cnblogs.com/Miracevin/p/9416253.html
Copyright © 2011-2022 走看看