引入

首先,有这样一道题:

给你一个单词W和一个文章T,问W在T中出现了几次(原题见POJ3461)。

OK,so easy~

HASH or KMP 轻松解决。

那么还有一道例题:

给定n个长度不超过50的由小写英文字母组成的单词准备查询,以及一篇长为m的文章,问:文中出现了多少个待查询的单词(原题见POJ3630)。

OK,依然so easy~

字典树(Trie)轻松解决。

那么,如果你说,什么是KMP和Trie,那么恭喜你啊……

建议大家要在看这篇博客之前做到:


进阶

如果你能看到这里,说明你已经熟练掌握了Trie和KMP。

那么现在可爱的动动扔给你了一道题:

给定n个长度不超过50的由小写英文字母组成的单词准备查询,以及一篇长为m的文章,问:文中出现了多少个待查询的单词。多组数据。

那么这个时候需要引入一个新的算法:AC自动机。

想必在所得诸位大佬一定有人听过这个名词。

首先简要介绍一下AC自动机(Aho-Corasick automation)(不是Accepted)。
就像当初我前队友gzh老师一样(状压 or 撞鸭?)。

该算法在1975年产生于贝尔实验室,是著名的多模匹配算法之一。

要搞懂AC自动机,先得有模式树(字典树)Trie和KMP模式匹配算法的基础知识。

KMP算法和AC自动机算法的区别在于,前者为单模式串匹配,而后者为多模式串匹配。

你可以感性的理解为,单模式串匹配就是只有一个子串,而多模式串匹配是指有不止一个子串。


主要步骤

三步走:

①将所有的模式串构成一颗Trie树。

②对Trie上所有的节点构造前缀指针。

③利用前缀指针Fail对主串进行匹配。

实际上这个前缀指针Fail与KMP算法中的nxt数组非常相似,因此AC自动机可以看作是Trie与KMP算法的结合(Trie上的KMP算法)。

第一步:字典树的构建想必大家都会了(如果不会的话你也不会读到这里),在此就不做过多的赘述。

第二步:

找Fail指针:

大家都知道,在KMP算法当中,当字符串发生失配时有nxt数组用来找到下一个匹配的位置,那么AC自动机中类似nxt数组的东西就是Fail指针,当发现失配的

字符失配的时候,跳转到Fail指针指向的位置,然后再次进行匹配操作,AC自动机之所以能实现多模式匹配,就归功于Fail指针的建立。

那么我们应该如何求Fail指针呢?

运用广度优先搜索 (BFS)来求得。

对于与根节点直接相连的点来说,如果这些节点失配,他们的Fail指针直接指向root即可。

其他节点其Fail指针求法如下:
设当前节点为father,其子节点为child。

求child的Fail指针时,首先我们要找到其father的Fail指针所指向的节点设为F,看F的孩子中有没有和child节点所表示的字母相同的节点,如果有的话,这个节

点就是child的Fail指针,如果没有,则需要再找到F的Fail指针所指向的节点,如果一直找都找不到,则child的Fail指针就要指向root。

例(帮助理解):

如图所示,首先root最初会进队,然后root,出队,我们把root的孩子的失败指针都指向root。因此图中h,s的失败指针都指向root,如红色线条所示,同时h,s进队。
接下来该h出队,我们就找h的孩子的Fail指针,首先我们发现h这个节点其Fail指针指向root,而root又没有字符为e的孩子,则e的Fail指针是空的,如果为空,则也要指向root,如图中蓝色线所示。并且e进队,此时s要出队,我们再找s的孩子a,h的Fail指针,我们发现s的Fail指针指向root,而root没有字符为a的孩子,故a的Fail指针指向root,a入队,然后找h的Fail指针,同样的先看s的Fail指针是root,发现root又字符为h的孩子,所以h的Fail指针就指向了第二层的h节点。e,a , h 的Fail指针的指向如图蓝色线所示。
此时队列中有e,a,h,e先出队,找e的孩子r的失败指针,我们先看e的失败指针,发现找到了root,root没有字符为r的孩子,则r的失败指针指向了root,并且r进队,然后a出队,我们也是先看a的失败指针,发现是root,则y的fail指针就会指向root.并且y进队。然后h出队,考虑h的孩子e,则我们看h的失败指针,指向第二层的h节点,看这个节点发现有字符值为e的节点,最后一行的节点e的失败指针就指向第三层的e。最后找r的指针,同样看第二层的h节点,其孩子节点不含有字符r,则会继续往前找h的失败指针找到了根,根下面的孩子节点也不存在有字符r,则最后r就指向根节点,最后一行节点的Fail指针如绿色虚线所示。
第三步:

文本串的匹配:

匹配过程分两种情况:

(1)当前字符匹配,从当前节点沿着树边有一条路径可以到达目标字符,如果当前匹配的字符是一个单词的结尾,就沿着当前字符的Fail指针,一直遍历到根,如果这些节点末尾有标记(当前节点单词末尾的标记),这些节点全都是可以匹配上的节点。统计完毕后,并将那些节点标记。此时只需沿该路径走向下一个节点继续匹配即可,目标字符串指针移向下个字符继续匹配;

(2)当前字符不匹配,则去当前节点失败指针所指向的字符继续匹配,当指针指向root时结束。

重复这2个过程中的任意一个,直到模式串走到结尾为止。

例:

还是刚才那张图:

假设其模式串为yasherhs。对于i=0,1。Trie中没有对应的路径,故不做任何操作;i=2,3,4时,指针p走到左下节点e。因为节点e的count信息为1,所以cnt+1,并且讲节点e的count值设置为-1,表示改单词已经出现过了,防止重复计数,最后temp指向e节点的失败指针所指向的节点继续查找,以此类推,最后temp指向root,退出while循环,这个过程中count增加了2。表示找到了2个单词she和he。当i=5时,程序进入第5行,p指向其失败指针的节点,也就是右边那个e节点,随后在第6行指向r节点,r节点的count值为1,从而count+1,循环直到temp指向root为止。最后i=6,7时,找不到任何匹配,匹配过程结束。

总结

三步:构造一棵Trie树,构造失败指针和模式匹配过程。

Fail指针≈nxt数组。

例题

「一本通 2.4 例 1」Keywords Search
「一本通 2.4 练习 1」玄武密码
「一本通 2.4 练习 3」单词
「一本通 2.4 练习 5」病毒
「一本通 2.4 练习 4」最短母串
「一本通 2.4 练习 6」文本生成器


例题1讲解

那么下面我来讲一下AC自动机的第一道例题Keywords Search。

题目传送门

这是一道板子题,其实上面的内容就是根据这道题来讲的,我在此就不做过多的赘述,主要讲一下代码。

#include<bits/stdc++.h>
using namespace std;
int n;
char s[2000001];
int trie[1000001][30];
int que[1000001],end[1000001],nxt[1000001];
int ans,cnt;
void insert(char *str)//Trie树构建过程
{
int p=1;
int len=strlen(str);
for(int i=0;i<len;i++)
{
int ch=str[i]-'a';
if(!trie[p][ch])
{
trie[p][ch]=++cnt;
memset(trie[cnt],0,sizeof(trie[cnt]));//每次只需要清空我们会用得到的行
}
p=trie[p][ch];
}
end[p]++;//因为有可能会有重复的单词,故在此end统计在此有多少个单词结束,而不是有没有单词结束
}
void build()//BFS构建Fail指针
{
for(int i=0;i<26;i++)//为了方便将0的所有转一遍都设为根节点1
trie[0][i]=1;
nxt[1]=0;//若在根节点失配, 则无法匹配字符
que[1]=1;
int head=1,tail=1;
while(head<=tail)
{
for(int i=0;i<26;i++)
if(!trie[que[head]][i])trie[que[head]][i]=trie[nxt[que[head]]][i];//注意这里,下面会有详细解释
else
{
que[++tail]=trie[que[head]][i];
int flag=nxt[que[head]];
while(flag&&!trie[flag][i])flag=nxt[flag];//循环往前找
nxt[trie[que[head]][i]]=trie[flag][i];
}
head++;//注意队头++
}
}
void find(char *str)//匹配
{
int p=1;
int len=strlen(str);
for(int i=0;i<len;i++)
{
int flag=p=trie[p][str[i]-'a'];
while(end[flag]!=-1&&flag)
{
ans+=end[flag];
end[flag]=-1;//标记这个点已经访问过,以后不再访问
flag=nxt[flag];
}
}
}
int main()
{
int T;
scanf("%d",&T);
while(T--)
{
memset(end,0,sizeof(end));//多测不清空,爆零两行泪(宝宝别哭)
cnt=1;
ans=0;
for(int i=0;i<26;i++)
trie[0][i]=1,trie[1][i]=0;//亦是清空
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%s",s);
insert(s);//读入子串并插入Trie树
}
build();
scanf("%s",s);
find(s);//匹配
printf("%d\n",ans);
}
return 0;
}

下面我来解释上面那个问题:

当发现不存在que[head](下面用u代替)的转移边i时,令trie[u][i]等于trie[nxt[u]][i],这并不符合Trie树的构造,但是在代码中却是正确的,那么这是为什么呢?

其实这是为了优化时间,若不存在trie[u][i]的转移边则指向trie[nxt[u]][i]。因为在具体问题中,若不存在trie[u][i]的转移边,往往需要沿que[head]的Fail指针走到第一个满足存在字符i的转移边的点v,得到trie[v][i],那么就直接将trie[u][i]赋值为trie[v][i],即trie[nxt[u]][i],这是求解的一类问题的时间优化。也正是这个原因,在构建Fail指针是,并没有处理v的转移边i不存在的情况,而是直接nxt[trie[u][i]]=trie[v][i](其中trie[v][i]也在之前就处理好了)。


rp++

AC自动机讲解+[HDU2222]:Keywords Search(AC自动机)的更多相关文章

  1. hdu2222 Keywords Search ac自动机

    地址:http://acm.split.hdu.edu.cn/showproblem.php?pid=2222 题目: Keywords Search Time Limit: 2000/1000 MS ...

  2. HDU2222 Keywords Search [AC自动机模板]

    Keywords Search Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 131072/131072 K (Java/Others ...

  3. HDU2222 Keywords Search —— AC自动机

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=2222 Keywords Search Time Limit: 2000/1000 MS (Java/O ...

  4. 【AC自动机】hdu2222 Keywords Search

    AC自动机模板题,给你n个模式串和一个文本串,问你有几个模式串在文本串出现过. 注意防止重复统计 这里推荐一波郭大爷的介绍,简单易懂. http://www.bilibili.com/video/av ...

  5. hdu2222 KeyWords Search AC自动机入门题

    /** 链接:http://acm.hdu.edu.cn/showproblem.php?pid=2222 题意:题意:给定N(N <= 10000)个长度不大于50的模式串,再给定一个长度为L ...

  6. hdu2222 Keywords Search (AC自动机板子

    https://vjudge.net/problem/HDU-2222 题意:给几个模式串和一个文本串,问文本串中包含几个模式串. 思路:贴个板子不解释. #include<cstdio> ...

  7. HDU2222 Keywords Search ac自动机第一题

    指针我一般都会出错,所以还是自己写数组版本. In the modern time, Search engine came into the life of everybody like Google ...

  8. HDU2222 Keywords Search 【AC自动机】

    HDU2222 Keywords Search Problem Description In the modern time, Search engine came into the life of ...

  9. 【HDU2222】Keywords Search AC自动机

    [HDU2222]Keywords Search Problem Description In the modern time, Search engine came into the life of ...

随机推荐

  1. 模板 - n个数的乘法逆元

    这道题里面不用保存 inva[i] ,而且还卡常.事实证明快读快到飞起, #include<bits/stdc++.h> using namespace std; typedef long ...

  2. 洛谷P4717 【模板】快速沃尔什变换(FWT)

    传送门 这玩意儿太骚了…… 参考了yyb巨佬的 //minamoto #include<iostream> #include<cstdio> #define ll long l ...

  3. [Xcode 实际操作]一、博主领进门-(13)在控制台的几种打印输出语句和po命令

    目录:[Swift]Xcode实际操作 本文将演几种在控制台输出日志的方式. 在项目导航区,打开视图控制器的代码文件[ViewController.swift] import UIKit class ...

  4. SpringBoot2.0基础案例(01):环境搭建和RestFul风格接口

    一.SpringBoot 框架的特点 1.SpringBoot2.0 特点 1)SpringBoot继承了Spring优秀的基因,上手难度小 2)简化配置,提供各种默认配置来简化项目配置 3)内嵌式容 ...

  5. iOS app支付宝接口调用的一点总结(补充支付宝SDK&Demo下载地址)

    由于app内需要用到支付功能,选择了当前最流行的支付宝进行支付.在进行内嵌支付宝功能开发时,被它狠狠的耍了一把. 根据支付宝开发文档,参考demo代码.将相关支付功能加到了自己的代码中.一些根据文档来 ...

  6. ZROI 部分题目题解

    ZROI 部分题目题解 335 首先发现一个性质: 对于最短的边而言,所有点的路径如果经过了这条边,那么路径的权值就是这条边的边权(废话) 那么我们把最短的边拎出来,可以发现,博物馆确定时,每个点按照 ...

  7. B.小A与任务

    链接:https://ac.nowcoder.com/acm/contest/369/B 题意: 小A手头有 n 份任务,他可以以任意顺序完成这些任务,只有完成当前的任务后,他才能做下一个任务 第 i ...

  8. Codeforces 1137B(kmp next数组构造)

    为了物尽其用,Next求出最多有哪部分能重复使用,然后重复使用就行了-- const int maxn = 5e5 + 5; char s[maxn], t[maxn]; int cnts0, cnt ...

  9. HDU6300(2018多校第一场)

    Bryce1010模板 http://acm.hdu.edu.cn/showproblem.php?pid=6300 排个序就好了 #include<iostream> #include& ...

  10. KEIL MDK之RTX的移植

    原文: http://lib.csdn.net/article/embeddeddevelopment/12240 一 将MDK安装目录的C:\keil\ARM\RL\RTX\Config下面的配置文 ...