zoukankan      html  css  js  c++  java
  • AC自动机详解

    首先,看清楚了,这是AC自动机不是自动AC机

    引用AC自动机模板题上的一句话:

     

    ovo


    在学习AC自动机之前,应该先掌握一下两个前置技能:

    AC自动机,通俗地讲,就是在Trie上跑KMP。AC自动机利用Trie的性质和KMP的思想,可以实现字符串的多模匹配。KMP是单模匹配,而它与AC自动机最大的区别就在fail指针的求法,其余思想基本相同。

    所谓多模匹配,即给出若干个模式串和一个文本串,需要查找这些模式串在文本串中出现的情况。

    AC自动机的操作分为三步:


    一、建树

    既然是要利用Trie,自然要先建立一棵Trie了。本文以she,he,say,her,shr五个字符串为例建立一棵Trie: 

    其中,root为根节点,绿色节点表示该节点为某个单词的结尾,也就是结尾标记。

    AC自动机的建树方法与Trie完全相同,在这里就不再赘述。

    建树代码:

    void add(string s)
    {
        int p=0;
        for(int i=0;i<s.size();i++)
        {
            if(!ac[p].son[s[i]-'a'])
                ac[p].son[s[i]-'a']=++tot;
            p=ac[p].son[s[i]-'a'];
        }
        ac[p].end++;
    }

    二、求fail指针

    求fail指针是AC自动机最精髓的地方,也是最大的难点。不过,在掌握了KMP算法之后,理解起来也不难。

    AC自动机中fail指针的作用与KMP中next数组的作用相同,都是在当前字符串失配时跳转到它指向的位置继续匹配。而AC自动机之所以能够进行多模匹配,就是因为fail指针。

    在AC自动机中,fail指针用BFS来求。

    步骤:

    1. 建立一个队列
    2. 将根的fail指针指向自己
    3. 将与根相连的节点的fail指针指向根,并将它们入队
    4. 取出队头h,遍历它的儿子。设当前遍历到的儿子节点为x,找到h节点的fail指针指向的节点,设其为k
    5. 若k有与x相同的儿子s,则将x的fail指针指向s;否则,找到k的fail指针,重复第5步,若一直都没有找到,则将x的fail指针指向根节点。将x入队,重复第4、5步,直到队列为空

    仍然以she,he,say,her,shr五个字符串为例,如图:

     

    1. 如图中红线所示,将与root相连的h、s的fail指针指向root并将它们入队
    2. 如图中蓝线所示,取出h,找到h的儿子e,因为h的fail指针指向root且root的儿子没有e,因此e的fail指针指向root,并将e入队;取出s,找到s的儿子a,因为s的fail指针指向root且root的儿子没有a,因此a的fail指针指向root,并将a入队;找到s的儿子h,因为h的fail指针指向root且root的儿子有h,因此h的fail指针指向与root相连的h,并将h入队
    3. 如图中绿线所示,取出e,找到e的儿子r,因为e的fail指针指向root且root的儿子没有r,因此r的fail指针指向root,并将r入队;取出a,找到a的儿子y,因为a的fail指针指向root且root的儿子没有y,因此y的fail指针指向root,并将y入队;取出h,找到h的儿子e,因为h的fail指针指向与root相连的h且该节点的儿子有e,因此e的指针指向与root相连的h的儿子e,并将e入队;找到h的儿子r,因为h的fail指针指向与root相连的h且该节点的儿子没有r,因此找到与root相连的h的fail指针指向的root,而root的儿子也没有r,因此r的指针指向root,并将r入队
    4. 最后,取出r,y,e,r,发现它们均没有儿子节点。此时队列为空,停止遍历。

    队列的状态是这样的:

    h s
    s e
    e a h
    a h r
    h r y
    r y e r

    这样讲可能有点乱,请结合图和队列状态理解,不会难。

    在实际实现过程中,若一直重复以上的第4、5步,时间复杂度难免会高。这里有一个巧妙的方法:当发现一个节点x没有某一个儿子s时,直接将s作为指针指向x的fail指针与s相同的这个儿子。这样实际上就是在模拟第4、5步反复查找的过程,这个指针会从上到下传递下来。因为当我们将根节点的编号设为0时,若一个节点没有儿子,就相当于这个儿子作为指针指向了根节点。这样可以更加方便地实现第4、5步。

    求fail指针代码:

    void build()
    {
        for(int i=0;i<26;i++)
            if(ac[0].son[i])
            {
                ac[ac[0].son[i]].fail=0;
                q.push(ac[0].son[i]);
            }//将与根相连的节点的fail指针指向根节点并将它们入队
        while(q.size())
        {
            int now=q.front();
            q.pop();//取出队头
            for(int i=0;i<26;i++)
                if(ac[now].son[i])
                {
                    ac[ac[now].son[i]].fail=ac[ac[now].fail].son[i];
                    q.push(ac[now].son[i]);
                }
                else
                    ac[now].son[i]=ac[ac[now].fail].son[i];
        }//重复第4、5步
    }

    三、字符串匹配

    字符串匹配的思想与KMP基本相同,实现方式与Trie上查找字符串类似。将文本串从头到尾一位一位在Trie上查找,对于每一个节点,若没有被遍历过,沿着它的fail指针走,直到根节点或一个已遍历过的点。对于路径上的所有点,将答案加上它的结尾标记,即当前节点为几个字符串的结尾,然后将其结尾标记改为-1,以显示其已遍历过。

    还是以这个图为例: 

    若文本串为yasherhs,则:

    1. 对于y,a,Trie中没有对应的路径
    2. 对于s,h,e,在Trie中可以沿着root-s-h-e这条路径走到第四层节点e,答案加1,沿着其fail路径向上可以走到第三层节点e,答案加1;
    3. 对于r,此时指针指向第四层节点e的儿子指向的节点,也就是其fail指针指向的第三层节点e,随后指向右下角节点r,答案加1;
    4. 对于h,s,因为已经遍历过了,因此不会再进行遍历

    为什么走到一个已遍历过的点也要停止呢?因为若一个节点已被遍历,则沿着它的fail指针走直到根节点的这条路径上的所有节点一定都被遍历过了,若在走一遍则属于浪费时间,因此直接停止即可。

    字符串匹配代码:

    int get()
    {
        int p=0,ans=0;
        for(int i=0;i<f.size();i++)
        {
            p=ac[p].son[f[i]-'a'];
            for(int j=p;j && ac[j].end!=-1;j=ac[j].fail)
            {
                ans+=ac[j].end;
                ac[j].end=-1;
            }
        }
        return ans;
    }

    最后奉上完整代码:

    #include<iostream>
    #include<string>
    #include<queue>
    using namespace std;
    const int N=1e6;
    int tot=0,n;
    string f;
    queue<int> q;
    struct T
    {
        int end=0,fail=0,son[26];分别表示结尾标记,fail指针和儿子节点
    }ac[N];
    void add(string s)
    {
        int p=0;
        for(int i=0;i<s.size();i++)
        {
            if(!ac[p].son[s[i]-'a'])
                ac[p].son[s[i]-'a']=++tot;
            p=ac[p].son[s[i]-'a'];
        }
        ac[p].end++;
    }//建树
    void build()
    {
        for(int i=0;i<26;i++)
            if(ac[0].son[i])
            {
                ac[ac[0].son[i]].fail=0;
                q.push(ac[0].son[i]);
            }
        while(q.size())
        {
            int now=q.front();
            q.pop();
            for(int i=0;i<26;i++)
                if(ac[now].son[i])
                {
                    ac[ac[now].son[i]].fail=ac[ac[now].fail].son[i];
                    q.push(ac[now].son[i]);
                }
                else
                    ac[now].son[i]=ac[ac[now].fail].son[i];
        }
    }//求fail指针
    int get()
    {
        int p=0,ans=0;
        for(int i=0;i<f.size();i++)
        {
            p=ac[p].son[f[i]-'a'];
            for(int j=p;j && ac[j].end!=-1;j=ac[j].fail)
            {
                ans+=ac[j].end;
                ac[j].end=-1;
            }
        }
        return ans;
    }//字符串匹配
    int main()
    {
        cin>>n;
        for(int i=1;i<=n;i++)
        {
            string s;
            cin>>s;
            add(s);
        }//输入模式串并插入Trie
        ac[0].fail=0;//将根节点的fail指针指向自己,其实这步可以不要,因为默认就是0
        build();//求fail指针
        cin>>f;
        cout<<get()<<endl;//输入文本串并匹配,直接输出答案
        return 0;
    }

    习题:


    声明:本文部分内容参考了一些大佬的博客

    参考资料:

    AC自动机算法详解 (转载)

    AC自动机-巨佬yyb


    2019.5.2 于厦门外国语学校石狮分校

  • 相关阅读:
    Windows 8实例教程系列 开篇
    qt 开发发布于 windeploy.exe
    qt qoci 测试验证
    vmware vmx 版本不兼容
    qt oracle
    vc qt dll
    QOCIDriver unable to create environment
    qoci 编译完 放置位置 具体根据情况
    calling 'lastError' with incomplete return type 'QSqlError' qsqlquer
    Hbase 操作工具类
  • 原文地址:https://www.cnblogs.com/TEoS/p/11384548.html
Copyright © 2011-2022 走看看