• AC自动机讲解


      AC自动机是处理多模式串匹配等一系列问题的工具,可以将它当做一种数据结构。

      首先我们考虑一个非常简单的问题,给定两个字符串,询问其中一个字符串在另一字符串中出现的次数,这个问题我们用KMP就可以非常容易的解决了,但是如果给定若干模式串,询问这组串在这个长串中的各个出现次数,这时KMP的时间复杂度就显然比较暴力了,对于这类的问题,我们可以考虑用AC自动机来解决(假设字符串中字符为大写字母)。

      定义指针。

    struct node{
        int cnt;
        node *fail,*child[30];
        node(){
            cnt=0; fail=NULL;
            memset(child,0,sizeof child);
        }
    };

      首先我们用一颗trie树将这若干个字符串存下来,这样方便我们处理多字符串匹配问题。

    void build_trie(){
        totnode=nodepool;
        root=totnode++;
        for (int i=1;i<=n;i++){
            char c[maxm];
            scanf("%s",&c);
            int len=strlen(c);
            node *t=root;
            for (int j=0;j<len;j++){
                if (!t->child[c[j]-'A']) t->child[c[j]-'A']=totnode++;
                t=t->child[c[j]-'A'];
            }
            t->cnt=1;
        }
    }

      这时候我们定义fail指针,他的作用类似与KMP中的next数组,代表在trie树上的某一节点如果失陪的话,需要向fail节点来转移,这样最优,即在模式串组中,假设当前节点代表的字符串为s,存在ss为s的后缀且ss最长,这时候s的fail指针指向ss的末节点。

      那么现在我们需要快速求出每一个点的fail指针,首先比较显然的是每一个点的fail指针指向的节点的深度都会小于i节点的深度,假设我们当前需要求第i个点的fail指针,且深度在i之前的点的fail指针我们都已经求出,那么对于i的父亲节点j,i为j的第k个儿子,如果j的fail指针有第k个儿子,那么i的fail指针就为j的fail指针的第k个儿子,否则向上找j的fail指针的fail指针的第k个儿子,因为j的fail指针指向的点代表的字符串为j点代表的字符串的最长后缀,那么肯定先考虑这个是最优的,j的fail指针的fail指针是j的次长后缀,这样我们从最长的依次考虑下去,所得到的i的fail就是最长解。

      因为我们需要保证在求第i个点之前,深度小于i的点都需要已经被处理,所以我们用一个队列来维护,首先将root入队,然后依次解决。

      root的fail指针为自己。

    void build_ac(){
       h=0;t=1; root
    ->fail=root; que[1]=root; //root入队 while (h<t){ node *u=que[++h]; for (int i=0;i<26;i++) if (u->child[i]){ //如果该节点有儿子,我们才求这个点的fail指针 que[++t]=u->child[i]; node *v=u->fail; for (;v!=root&&!v->child[i];v=v->fail); //找到que[t]的fail指针。 que[t]->fail=v->child[i]&&v->child[i]!=que[t]?v->child[i]:root; //因为root先入队,所以root的儿子的父亲的fail指针为root,这样root儿子的fail指针就指向了自己,防止这种情况我们特判下 } } }

      但是首先我们明确自动机的定义,在每一个状态的每一个转移,我们都应该有转移方法,即每个点的所有儿子都不应该为空,虽然上一种构建ac自动机的方法对于某些问题也正确,但第一不满足自动机的定义,第二每次求每个点的fail指针时,我们向上寻找了好多次,这样就加大了ac自动机的常数,所以我们通常使用另一种ac自动机的构建方式。

    void build_ac(){
        int h=0,t=1;
        que[1]=root; root->fail=root; 
        for (int i=0;i<26;i++) if (!root->child[i]) root->child[i]=root; //root的每一个空儿子都应该指向root
        while (h<t){
            node *v=que[++h];
            for (int i=0;i<26;i++) if (v->child[i]&&v->child[i]!=root){ //因为root的空儿子定义为root,所以我们需要特判
                que[++t]=v->child[i];
                node *u=v->fail;
                que[t]->fail=u->child[i]!=que[t]?u->child[i]:root; //上一种因为v->fail的每一个儿子都非空,所以我们直接赋值就行了。
            } else v->child[i]=v->fail->child[i]; //对于这种本来是空儿子的节点,我们将他的空儿子连到该节点fail指针的儿子,这样就保证了fail指针直接赋值的正确性。
        }
    }

      而且对于fail指针,每个节点都有一个fail指针,这类似与树的定义,所以我们将fail指针反向,可以建立failtree。

      现在我们拥有了ac自动机,那么对于询问一个字符串组在一个字符串中出现的次数的问题,我们可以在ac自动机中模拟建立trie树的方式,跑这个字符串,将这个字符串经过的所有点的cnt++,建立failtree,每个节点的子树权值和就是该串在长串中出现次数。

      那么对于单词(tjoi)这个题,我们需要考虑一个字符串组中,每个字符串在字符串组中出现的次数,这时假设当前字符串的结尾节点为i,所有指向i的fail指针所对应的节点都会使i的出现次数增加该节点的cnt次,那么建立failtree求和即可,其实简单的,我们可以将每一个字符串经过的节点的cnt++,这样我们从最深层开始

    t->fail-cnt+=t->cnt,这样可以达到相等的效果。

      关于ac自动机上的dp问题,最经典的就是给定字符串组,求一个长度问m的穿,(不)包含字符串组中的字符串的方案数,这样我们只需要定义状态w[0..1][i][j]代表当前在ac自动机上为第i个点,构建的字符长度为j的时候,包括(1),不包括字符串组中的字符串的方案数,直接累加就行了。

  • 相关阅读:
    rails3 routes
    rails delete destroy difference
    ruby doc
    今天提交了一个patch开心,呵呵
    ruby collect map seems the function is the same?
    jquery closest
    rails 笔记
    网店系统
    rails脚本架命令及心得
    rails3 expericence
  • 原文地址:https://www.cnblogs.com/BLADEVIL/p/3562187.html
Copyright © 2020-2023  润新知