首页 > 其他 > 详细

浅析 AC 自动机

时间:2020-04-05 21:15:38      阅读:68      评论:0      收藏:0      [点我收藏+]

简述

我竟然会了如此高深的算法的皮毛

根据众多神犇的 blog 所说,学习 AC 自动机前要学习两个东西:

  1. trie 树。(可以康康我的 blog trie 树学习笔记
  2. KMP。(同样可以康康我的 blog KMP 学习笔记

既然我们都已经会了以上两个数据结构/算法,为什么不进一步学习呢?

现在开始进入正题。

作用

首先我们要知道 AC 自动机使用来干什么的。

一定要记住:这不是一个可以自动帮你 AC 题目的算法。(如有需要请搜索“自动 AC 机”)

那么为什么叫 AC 自动机呢,以下来自某度某科:

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

好的,这是一个多模匹配算法,那么什么是多模匹配呢?

首先我们知道 KMP 是一种高效的单模匹配算法,就是说从一个文本串中找到一个模式串的所在位置

那么 AC 自动机就是用来求解:从一个文本串中找到多个模式串的所在位置

当然,最直接的方法是 暴力 或者 多次KMP,但是复杂度无疑都会爆炸,所以就有了这个高效的多模匹配算法。

初步了解

首先,我们要输入 \(N\) 个模式串,举个栗子:hit,her,his,she。首先建立一棵 trie 树。

技术分享图片

就是酱紫的啦。

然后 ... 。我们来一个神奇的操作:

\(fail\) 是失配指针,注意是失配意味着如果此时匹配失败,那么我们就要到达这个指针指向的位置继续常数匹配。

所以,我们可以将失配指针指向的的节点理解为:

当前节点所代表的串,最长的、能与后缀匹配的,在 trie 中出现过的前缀所代表的节点。

所以,\(fail\) 指针类似于 KMP 的 \(next[]\) 数组,只不过由单串变为了多串而已。

加入失配指针后就是酱紫:

技术分享图片

看起来很乱但是很有用的啦,虽然画在了这里但是并不知道怎么求对不对

我们先看看这个指针是要干什么吧。

每次沿着 Trie 树匹配,匹配到当前位置没有匹配上时,直接跳转到失配指针所指向的位置继续进行匹配。

然后 dalao 们的 blog 告诉我们 Trie 树的失配指针是:

指向沿着其父节点 的 失配指针,一直向上,直到找到拥有当前这个字母的子节点 的节点 的那个子节点。

看不懂?那就继续往下看吧。

原理分析

简单说就是在 trie 树上跑 KMP,那么显然最难的地方就是找失配指针。(就是上面那个五颜六色的东东)

如果您确保已经理解了 KMP 的 \(next[]\) 数组的含义的话,这就很好理解了。

这就是在失配之后应该去的地方,也因为有这个数组的存在,才让 AC 自动机有了如此高的效率。

这个只能意会而不可言传,理解起来比较难,强烈推荐读者自行画图理解。(或借助上图理解)

然后是匹配,其实匹配的过程十分简单,直接讲述一下即可首先。

指针指向根节点依次读入单词,检查是否存在这个子节点。

然后指针跳转到子节点,如果不存在,直接跳转到失配指针即可。

是不是感觉和 KMP 好像的样子。

代码实现

显示模板式的 trie 插入:

void insert(string s){
	int p=1,len=s.length();
	for(int i=0;i<len;i++){
		int ch=s[i]-‘a‘;
		if(!trie[p][ch]) trie[p][ch]=++tot;
		p=trie[p][ch];
	}
	sum[p]++;//注意一下可能有重复串。
	return;
}

然后是最重要的 \(get\_fail()\) 函数,详情请见注释:

void get_fail(){
	queue<int>q;
	for(int i=0;i<26;i++){//第二层的fail指针提前处理一下。
		if(!trie[1][i]) continue;
		q.push(trie[1][i]);//加入队列,进行 BFS。
		fail[trie[1][i]]=1;//将第二层的 fail 指针指向根节点。
	}
	while(!q.empty()){
		int now=q.front();
		q.pop();
		for(int i=0;i<26;i++){//枚举所有子节点。
			if(trie[now][i]){//存在这个子节点。
				fail[trie[now][i]]=trie[fail[now]][i];
                //子节点的 fail 指针指向当前节点的。
                //fail 指针所指向的节点的相同子节点。
				q.push(trie[now][i]);//入队。
			}
			else trie[now][i]=trie[fail[now]][i];//不存在这个子节点。
            //当前节点的这个子节点指向当前节点 fail 指针的这个子节点。
            //唯一变化是没有入队。(根本不存在为什么要 BFS 对吧)
		}
	}
	return;
}

然后是匹配的代码:

int AC_ask(string a){
	int p=1,ans=0,len=a.length();
	for(int i=0;i<len;i++){//根据匹配串的字符进行遍历。
		p=trie[p][a[i]-‘a‘];
		for(int j=p;j && sum[j]!=-1;j=fail[j]){//对每一个字符进行遍历。
            //注意一下循环条件,第一个是没有指向空(j),第二个是之前没有遍历过(sun[j]!=-1)
            //注意之后不是 j++,而是 j=fail[j],即走到自己的 fail 指针,直到其为空。
			ans+=sum[j];//统计答案。
            sum[j]=-1;//将其标记为已经经过,不用进行重复遍历。
		}
	}
	return ans;//返回答案。
}

最后是 main 函数的代码:

int n,trie[N][30],sum[N],fail[N],tot=1;
string s,a;

int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		cin>>s;
		insert(s);//插入。
	}
	cin>>a;
	get_fail();//找 fail[] 指针。
	printf("%d\n",AC_ask(a));//输出。
    return 0;
}

更进一步

其实...就是多做题,没有什么窍门的好吗。

然后先是模板题,用上面的代码就可以过了。

其他的...之后补锅吧。

结语

之前说了,我的了解只是皮毛。

想要更进一步的话,可以去看看集训队的论文。(2004 多串匹配算法及其启示——朱泽园)

网上应该是搜的到的,如果没有可以私信或者 QQ 找我要,我会不会回就看情况了。(QQ 自己去翻这篇文章

注意,本文部分解释和我的 AC 自动机启蒙来自yyb 神犇的文章,侵权请删。

完结撒花。

浅析 AC 自动机

原文:https://www.cnblogs.com/lpf-666/p/12638777.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!