【谈谈知识点】AC自动机

前言

一言不合就开坑,说的就是我~
之前觉得这东西挺难,然后某天早上花了一个半小时就学会了……

P.S:如果你诚心诚意的想学会AC自动机,一定要先去看懂KMP和Trie。
而且一定要好好理解KMP的next~

1.What is AC自动机?

就是通过不懈的努力,最终发明了自动帮你A题的算法
如果需要相关信息可以去百度百科一些知识,比如什么是自动机,什么是AC……

我在这里只说一下自己的看法:

从应用角度来说:这个东西主要用于解决多个串的匹配问题,最经典的例题就是:
找给定的n个串在给定的某文本中(出现了多少次)/(各出现多少次 )

从算法角度来说:如同绝大多数人说的那样,这其实就是Trie树+KMP而已。

那么看这篇文章请随时记住几个关键词:
前缀、失配

2.How to do it?

我们还是搬出经典的例题:

给定n个模式串和1个文本串,求有多少个模式串在文本串里出现过。

有没有什么想法?
容易想到的暴力是跑n遍KMP,每个串配一次,或者是建个trie树,也是跑n遍。
不过这个数据范围一点也不友好:

∑ len(模式串)<=106,length(文本串)<=106

这个时候AC自动机就发挥作用了,它的时间复杂度为 O(能过)
其实是我不会分析复杂度不信你看以前的也没有
那么构造一个AC自动机需要以下三步:

1、造一个Trie树

啊,对,就是造个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++;
}
2、构造失配指针

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-匹配成功,变成了直接匹配成功~

3、运行AC自动机

好了,树也有了,指针也好了,我们直接放上去跑就完事,不多说了:

//这是统计一共出现了多少个模式串
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;
		}
	}
}

3、What can be solved?

AC自动机的运用范围主要是字符串相关,尤其是多串匹配的时候非常优秀,只不过面对某些毒瘤的要求或许需要改变统计的方法,或者是失配指针的构造法。
其实最难的是看出这是AC自动机

最后推荐一道AC自动机的好题,可以大程度加深对AC自动机的理解:
P2444 [POI2000]病毒 -

发布了44 篇原创文章 · 获赞 16 · 访问量 7265

猜你喜欢

转载自blog.csdn.net/Cyan_rose/article/details/85396375