AC自动机算法 && 例题
参考链接:
https://blog.csdn.net/bestsort/article/details/82947639#commentBox
https://blog.csdn.net/niushuai666/article/details/7002823
AC自动机简介:
首先简要介绍一下AC自动机:Aho-Corasick automation,该算法在1975年产生于贝尔实验室,是著名的多模匹配算法之一。一个常见的例子就是给出n个单词(这n个单词,我们称它为模式串),再给出一段包含m个字符的文章(我们称它为主串),让你找出有多少个单词在文章里出现过。要搞懂AC自动机,先得有字典树Trie和KMP模式匹配算法的基础知识。KMP算法是单模式串的字符匹配算法,AC自动机是多模式串的字符匹配算法。
ac自动机时间复杂度:
假设有N个模式串,平均长度为L;文章长度为M。 建立Trie树:O(N*L) 建立fail指针:O(N*L) 模式匹配:O(M*L) 所以,总时间复杂度为:O( (N+M)*L )。
AC自动机构造:
1、首先用n个单词构造一颗字典树
2、为树上的每一个节点设一个失败指针(代码中用fail[]数组来表示),这个失败指针和KMP算法有点类似。用KMP算法来找的时候如果匹配失败会通过next数组跳到下一个位置进行匹配。失败指针也是这个用途,它就是此次匹配失败后跳到下一个位置进行下一次匹配
3、扫描主串进行匹配
具体AC自动机构造:
首先我们需要建立一棵Trie。但是这棵Trie不是普通的Trie,而是带有一些特殊的性质。
首先会有3个重要的指针,分别为p, p->fail, temp。
1.指针p,指向当前匹配的字符。若p指向root,表示当前匹配的字符序列为空。(root是Trie入口,没有实际含义)。
2.指针p->fail,p的失败指针,指向与字符p相同的结点,若没有,则指向root。
3.指针temp,测试指针(自己命名的,容易理解!~),在建立fail指针时有寻找与p字符匹配的结点的作用,在扫描时作用最大,也最不好理解。
对于Trie树中的一个节点,对应一个序列s[1...m]。此时,p指向字符s[m]。若在下一个字符处失配,即p->next[s[m+1]] == NULL,则由失配指针跳到另一个节点(p->fail)处,该节点对应的序列为s[i...m]。若继续失配,则序列依次跳转直到序列为空或出现匹配。在此过程中,p的值一直在变化,但是p对应节点的字符没有发生变化。在此过程中,我们观察可知,最终求得得序列s则为最长公共后缀。另外,由于这个序列是从root开始到某一节点,则说明这个序列有可能是某些序列的前缀。
fail指针构造:
首先给定模式串"ash","shex","bcd","sha"
,然后我们根据模式串建立如下trie树:
然后我们再了解下一步:
ac自动机,就是在tire树的基础上,增加一个fail指针,如果当前点匹配失败,则将指针转移到fail指针指向的地方,这样就不用回溯,而可以路匹配下去了.(当前模式串后缀和fail指针指向的模式串部分前缀相同,如abce和bcd,我们找到c发现下一个要找的不是e,就跳到bcd中的c处,看看此处的下一个字符(d)是不是应该找的那一个)
一般,fail指针的构建都是用bfs实现的
首先每个模式串的首字母(也就是根节点的直系子节点)肯定是指向根节点的
现在第一层bfs遍历完了,开始第二层
(根节点为第0层)第二层a
的子节点为s
,但是我们还是要从a-z
遍历,如果不存在这个子节点我们就让他指向根节点(如下图红色的a
)
当我们遍历到s
的时候,由于它的父亲节点(即、a)的fail指针指向的节点(即、根)的直系子节点存在s
这个节点,我们就让他的fail指针指向他父亲节点(a
)的fail指针指向的那个节点(根
)的具有相同字母的子节点(第一层的s
),也就是这样(没错fail指针就是这样构造的
按照相同规律构建第二层后,到了第三层的h
点,还是按照上面的规则,我们找到h
的父亲节点(s
)fail指针指向的那个位置(第一层的s
)然后指向它所指向的相同字母根->s->h
的这个链的h
节点,如下图
完全构造好后的树
然后匹配就很简单了,这里以ashe为例
我们先用ash匹配,到h了发现:诶这里ash是一个完整的模式串,好的ans++,然后找下一个e,可是ash后面没字母了啊,我们就跳到hfail指针指向的那个h继续找,还是没有e?再跳,结果当前的h指向的是根节点,又从根节点找,然而还是没有找到e,程序END
扫描主串:
构造好Trie和失败指针后,我们就可以对主串进行扫描了。这个过程和KMP算法很类似,但是也有一定的区别,主要是因为AC自动机处理的是多串模式,需要防止遗漏某个单词,所以引入temp指针。
匹配过程分两种情况:(1)当前字符匹配,表示从当前节点沿着树边有一条路径可以到达目标字符,此时只需沿该路径走向下一个节点继续匹配即可,目标字符串指针移向下个字符继续匹配;(2)当前字符不匹配,则去当前节点失败指针所指向的字符继续匹配,匹配过程随着指针指向root结束。重复这2个过程中的任意一个,直到模式串走到结尾为止。
扫描代码:
1 int len=strlen(s); 2 int now=root; 3 int res=0; 4 for(int i=0;i<len;++i) 5 { 6 now=next[now][s[i]-'a']; //这个now就保证了我们找的肯定是这个s字符串中的一部分 7 int temp=now; //这就是一直找,找到了就加1,找不到就通过失败指针看其他地方能不能找到 8 while(temp!=root) //尝试这个节点的失败指针能不能找到模式串 9 { 10 res+=ends[temp]; //因为我们要找出来s串中包含几种模式串,所以要有赋值0操作 11 ends[temp]=0; 12 temp=fail[temp]; 13 } 14 } 15 return res;
例题: