AC自动机讲解
AC自动机讲解
AC自动机讲解
首先,作为作者的我一定知道你已经会了这两个算法:KMP与Trie树,如若不会,可以先学习一下。
我在这里声明一下AC自动机不是自动AC的算法,其全称是Aho-Corasick automaton,是一种著名的多模匹配算法。其实现类似于Trie树与KMP算法的结合,是将多个模式串放在Trie树上做出fail指针,并用文本串进行匹配。整个算法共分为三个部分:1) 建Trie树 2) 匹配fail指针 3) 运用并求出答案。下面我会讲解前两个步骤。
建Trie树
相信学过Trie树的OIer都会,就是简单的动态开点,如果当前节点的出边有下一个字符,则直接走下去,否则我们可以新开一个节点,再走下去,是不是和动态开点线段树思想一样?代码也比较好实现,在这里我以都是小写英文字母为例。
#define N 1000001 char str;int root,cnt; struct Node {int son[26];}node[N]; //每一个结构体都是一个节点,son数组是当前节点的儿子们 void build(int now,int p) { if(now>=lenth) return; //当遍历结束当前字符串之后是要返回的,不能继续遍历下去,记住字符串是从0开始的 int s=str[now]-\'a\'; if(!node[p].son[s]) node[p].son[s]=++cnt; //如果没有节点,新开一个节点就好了 build(now+1,node[p].son[s]); //继续遍历 } int main() { scanf("%s",str),lenth=strlen(str),build(0,root); }
匹配fail指针
这是整个算法的核心部分,也是我要重点讲解的部分,我们看一下下面左面的图片,这是一棵已经建好的Trie树,分别是五个模式串所建成的Trie树(五个模式串为:aaa、abc、bcd、cab、cad),我们就要用这个简单的Trie树来学习什么是fail指针。首先我们要弄明白fail指针的定义,AC自动机的fail指针和KMP中的fail指针大致相同,但是还是有一定的区别的,这里的fail指针指向的是一个字符串中的一个点,并且当前字符串的后缀与以指向的点为结尾的后缀相同的长度最长,见下面右图中,已经给出例子来了,理解一下定义。
那么它是怎么匹配出来的呢?我们发现每一个点的fail指针都是指向上面,即层数小于自己的点,所以我们不能用DFS来更新,应该用BFS这种优秀的算法,这样我们每一次更新点的时候,都能用已经更新好点,我们再看一看图,发现当前点的fail的儿子与自己的儿子有相同时,自己的儿子的fail就是自己的fail的儿子,图中有不只一组满足这个性质,根据这个性质,我们寻找fail指针就容易多了。下面就是代码部分了。
void find_fail() { queue <int> q; for(int i=1;i<=26;i++) if(node[root].son[i]) q.push(node[root].son[i]); while(!q.empty()) { int p=q.front();q.pop(); for(int i=1;i<=26;i++) { if(node[p].son[i]) { node[node[p].son[i]].fail=node[node[p].fail].son[i]; q.push(node[p].son[i]); } else node[p].son[i]=node[node[p].fail].son[i]; } } }
在这里,我用的是系统之中的队列,如果有兴趣手写的话,可以手写,我不拦着。
有人看过代码,可能会问,那个else是什么鬼?在这里我解释一下,如果当前节点的fail有自己没有的儿子,我们可以把fail的儿子接在自己上,这样在更新答案时,就可以直接递归的找下去,不用再跳到fail上之后再递归儿子,这是一个小小的优化。
剩下的就是应用了,应用就不细说了,具体题目具体分析,不会的可以发评论问我。