一言不合就开坑,说的就是我~
之前觉得这东西挺难,然后某天早上花了一个半小时就学会了……
P.S:如果你诚心诚意的想学会AC自动机,一定要先去看懂KMP和Trie。
而且一定要好好理解KMP的next~
就是通过不懈的努力,最终发明了自动帮你A题的算法
如果需要相关信息可以去百度百科一些知识,比如什么是自动机,什么是AC……
我在这里只说一下自己的看法:
从应用角度来说:这个东西主要用于解决多个串的匹配问题,最经典的例题就是:
找给定的n个串在给定的某文本中(出现了多少次)/(各出现多少次 )
从算法角度来说:如同绝大多数人说的那样,这其实就是Trie树+KMP而已。
那么看这篇文章请随时记住几个关键词:
前缀、失配
我们还是搬出经典的例题:
给定n个模式串和1个文本串,求有多少个模式串在文本串里出现过。
有没有什么想法?
容易想到的暴力是跑n遍KMP,每个串配一次,或者是建个trie树,也是跑n遍。
不过这个数据范围一点也不友好:
∑ len(模式串)<=106,length(文本串)<=106;
这个时候AC自动机就发挥作用了,它的时间复杂度为 O(能过)
其实是我不会分析复杂度不信你看以前的也没有
那么构造一个AC自动机需要以下三步:
啊,对,就是造个Trie树,没有什么特殊的地方,老老实实insert就完了。
struct Node{int son[27],num;}node[N<<2];
ivoid insert(char *c,int rank)
{
int len=strlen(c),now=0;
for(rint i=0;i<len;i++){
int d=c[i]-'a';
if(!node[now].son[d]){
node[now].son[d]=++cnt;
}
now=node[now].son[d];
}
node[now].num++;
}
AC自动机的重点难点就是在这个地方:怎样把KMP的失配指针的思想给用上?
我们来考虑一下上面那个例题在做的时候需要怎样的优化:
假设给定的模式串是:SHE HE HER SAY SHR
我们构建出来的Trie树该是这个样子
(图片来源:http://www.cnblogs.com/cjyyb/p/7196308.html) (另作了修改)
①假设我们的文本串是 SHA
那么按照常规方法,我们会检索到S,走S-H,失配,检索到H,走H-A。
现在失配指针就要派上用场了!我们希望S-H之后直接跳到旁边的H-A去,以此来节约时间。我们敢这么干的原因是什么?原因之一是因为两边都有H。
那么失配指针构造的显而易见的原则之一:出现相同字母
②假设我们的文本串是SHER
常规就不说了
这里的失配指针自然是从SHE的E指向HER的E。那么问题来了,我们能不能从HER的E跳到SHE的E呢?显然不行。
原因很简单,HER的前缀不一定有S,可能不符合匹配要求
(为什么说不一定,因为AC自动机进行的是多次匹配,是可能存在之前出现S的情况的);
因而我们加失配指针的原则还有一个:从深度小的往深度大的加。
那么我们分析倒是分析了,实际构造又该怎样呢?我们结合代码,分段来看一看:
ivoid build()
{
queue<int>q1;
for(rint i=0;i<26;i++){
if(node[0].son[i]){
q1.push(node[0].son[i]);
}
}
在这里我们从根节点0开始,把它的所有子节点加入队列中。
while(!q1.empty()){
int now=q1.front();q1.pop();
for(rint i=0;i<26;i++){
if(node[now].son[i]){
fail[node[now].son[i]]=node[fail[now]].son[i];
q1.push(node[now].son[i]);
}
此处正在对每一个点的儿子进行检索,如果有这个儿子,那么把他的失配指针指向他父亲的失配指针指向的点的对应儿子。
emm也许有些不好理解,我们分类讨论:
那么情况一:父亲失配指针指向的点没有这个儿子,也就是继续失配。由于空节点的编号和根节点编号相同,相当于是指向了根节点。
情况二:xxxx的点有这个儿子,那么我们可以通过失配指针跳过去,再度向下寻找可以继续匹配的节点,没有就继续跳……一直到匹配成功或者回到根节点。
(其实这个地方大家可以想一想前向星的存边方法,跟这个失配指针不断跳是很类似的~)
else node[now].son[i]=node[fail[now]].son[i];
啥啥啥?为啥你把儿子共用了???
其实这地方的朴素写法是这样的:
else fail[node[now].son[i]=node[fail[now]].son[i];
但是本身你这个儿子就是空的,如果我们在运行的时候通过失配指针跳到这里,会发现完全没法匹配(空节点哪里来的儿子),于是就继续往后跳。
这是白白浪费了时间。因此我们就直接共用子节点, 只不过是从失配-跳一下fail-发现为空-继续跳fail-匹配成功,变成了直接匹配成功~
好了,树也有了,指针也好了,我们直接放上去跑就完事,不多说了:
//这是统计一共出现了多少个模式串
ivoid check(char *c)
{
int len=strlen(c),now=ans=0;
for(rint i=0;i<len;i++){
now=node[now].son[c[i]-'a'];
for(rint j=now;j&&node[j].num;j=fail[j]){
ans+=node[j].num;
node[j].num=0;
}
}
}
AC自动机的运用范围主要是字符串相关,尤其是多串匹配的时候非常优秀,只不过面对某些毒瘤的要求或许需要改变统计的方法,或者是失配指针的构造法。
(其实最难的是看出这是AC自动机 )
最后推荐一道AC自动机的好题,可以大程度加深对AC自动机的理解:
P2444 [POI2000]病毒 -