AC自动机详解

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

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

ovo



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

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

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

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

  • 建树
  • 求fail指针
  • 字符串匹配


一、建树

既然是要利用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 于厦门外国语学校石狮分校

原文地址:https://www.cnblogs.com/TEoS/p/11384548.html

时间: 2024-09-29 22:23:52

AC自动机详解的相关文章

[转] AC自动机详解

转载自:http://hi.baidu.com/nialv7/item/ce1ce015d44a6ba7feded52d AC自动机是用来处理多串匹配问题的,即给你很多串,再给你一篇文章,让你在文章中找这些串是否出现过,在哪出现.也许你考虑过AC自动机名字的含义,我也有过同样的想法.你现在已经知道KMP了,他之所以叫做KMP,是因为这个算法是由Knuth.Morris.Pratt三个提出来的,取了这三个人的名字的头一个字母.那么AC自动机也是同样的,他是Aho-Corasick.所以不要再YY地

Aho-Corasick 多模式匹配算法、AC自动机详解

Aho-Corasick算法是多模式匹配中的经典算法,目前在实际应用中较多. Aho-Corasick算法对应的数据结构是Aho-Corasick自动机,简称AC自动机. 搞编程的一般都应该知道自动机FA吧,具体细分为:确定性有限状态自动机(DFA)和非确定性有限状态自动机NFA.普通的自动机不能进行多模式匹配,AC自动机增加了失败转移,转移到已经输入成功的文本的后缀,来实现. 1.多模式匹配 多模式匹配就是有多个模式串P1,P2,P3...,Pm,求出所有这些模式串在连续文本T1....n中的

【转】AC算法详解

原文转自:http://blog.csdn.net/joylnwang/article/details/6793192 AC算法是Alfred V.Aho(<编译原理>(龙书)的作者),和Margaret J.Corasick于1974年提出(与KMP算法同年)的一个经典的多模式匹配算法,可以保证对于给定的长度为n的文本,和模式集合P{p1,p2,...pm},在O(n)时间复杂度内,找到文本中的所有目标模式,而与模式集合的规模m无关.正如KMP算法在单模式匹配方面的突出贡献一样,AC算法对于

后缀自动机详解 -----转载

转载于:http://blog.csdn.net/qq_35649707/article/details/66473069 原论文(俄文)地址:suffix_automata 后缀自动机 后缀自动机(单词的有向无环图)——是一种强有力的数据结构,让你能够解决许多字符串问题. 例如,使用后缀自动机可以在某一字符串中搜索另一字符串的所有出现位置,或者计算不同子串的个数——这都能在线性 时间内解决. 直觉上,后缀自动机可以被理解为所有子串的简明信息.一个重要的事实是,后缀自动机以压缩后的形式包含了一个

算法模板——AC自动机

实现功能——输入N,M,提供一个共计N个单词的词典,然后在最后输入的M个字符串中进行多串匹配(关于AC自动机算法,此处不再赘述,详见:Aho-Corasick 多模式匹配算法.AC自动机详解.考虑到有时候字典会相当稀疏,所以引入了chi和bro指针进行优化——其原理比较类似于邻接表,这个东西本身和next数组本质上是一致的,只是chi和bro用于遍历某一节点下的子节点,next用于查询某节点下是否有需要的子节点) 1 type 2 point=^node; 3 node=record 4 ex:

AC自动机算法

AC自动机简介:  首先简要介绍一下AC自动机:Aho-Corasick automation,该算法在1975年产生于贝尔实验室,是著名的多模匹配算法之一.一个常见的例子就是给出n个单词,再给出一段包含m个字符的文章,让你找出有多少个单词在文章里出现过.要搞懂AC自动机,先得有字典树Trie和KMP模式匹配算法的基础知识.KMP算法是单模式串的字符匹配算法,AC自动机是多模式串的字符匹配算法. AC自动机的构造: 1.构造一棵Trie,作为AC自动机的搜索数据结构. 2.构造fail指针,使当

转载 - AC自动机算法

出处:http://blog.csdn.net/niushuai666/article/details/7002823 AC自动机简介:  首先简要介绍一下AC自动机:Aho-Corasick automation,该算法在1975年产生于贝尔实验室,是著名的多模匹配算法之一.一个常见的例子就是给出n个单词,再给出一段包含m个字符的文章,让你找出有多少个单词在文章里出现过.要搞懂AC自动机,先得有字典树Trie和KMP模式匹配算法的基础知识.KMP算法是单模式串的字符匹配算法,AC自动机是多模式

数据结构14——AC自动机

一.相关介绍 知识要求 字典树Trie KMP算法 AC自动机 多模式串的字符匹配算法(KMP是单模式串的字符匹配算法) 单模式串问题&多模式串问题 单模就是给你一个模式串,问你这个模式串是否在主串中出现过,这个问题可以用kmp算法高效完成: 多模就是给你多个模式串,问你有多少个模式串在这个主串中出现过. 若我们暴力地用每一个模式串对主串做kmp,这样虽然理论上可行,但是时间复杂度非常之高.而AC自动机算法就能高效地处理这种多模式串问题. 二.算法实现 [打基础] 失配指针fail 每个节点都有

AC自动机算法详解

首先简要介绍一下AC自动机:Aho-Corasick automation,该算法在1975年产生于贝尔实验室,是著名的多模匹配算法之一.一个常见的例子就是给出n个单词,再给出一段包含m个字符的文章,让你找出有多少个单词在文章里出现过.要搞懂AC自动机,先得有模式树(字典树)Trie和KMP模式匹配算法的基础知识.AC自动机算法分为3步:构造一棵Trie树,构造失败指针和模式匹配过程.     如果你对KMP算法和了解的话,应该知道KMP算法中的next函数(shift函数或者fail函数)是干