AC自动机算法及模板

  • 关于AC自动机
  1. AC自动机:Aho-Corasickautomation,该算法在1975年产生于贝尔实验室,是著名的多模匹配算法之一。一个常见的例子就是给出n个单词,再给出一段包含m个字符的文章,让你找出有多少个单词在文章里出现过。要搞懂AC自动机,先得有模式树(字典树)Trie和KMP模式匹配算法的基础知识。AC自动机算法分为3步:构造一棵Trie树,构造失败指针和模式匹配过程。
  2. 简单来说,AC自动机是用来进行多模式匹配(单个主串,多个模式串)的高效算法。
  • AC自动机的构造过程

使用Aho-Corasick算法需要三步:

  1. 建立模式串的Trie
  2. 给Trie添加失败路径
  3. 根据AC自动机,搜索待处理的文本

我们以下面这个例子来介绍AC自动机的运作过程

这里以 hdu 2222 KeywordsSearch 这一道题最为例子进行讲解,其中测试数据如下:

给定5个单词:say she shr he her,然后给定一个字符串  yasherhs。问一共有多少单词在这个字符串中出现过。

  • 确定数据结构

首先,我们需要确定AC自动机所需的数据存储结构,它们的用处之后会讲到。

struct Node
{
	int cnt;//是否为该单词的最后一个结点
	Node *fail;//失败指针
	Node *next[26];//Trie中每个结点的各个节点
}*queue[500005];//队列,方便用BFS构造失败指针
char s[1000005];//主字符串
char keyword[55];//需要查找的单词
int head,tail;
Node *root;//头结点 

第一步:构建Trie

根据输入的 keyword 一 一 构建在Trie树中

void Build_trie(char *keyword)//构建Trie树
{
	Node *p,*q;
	int i,v;
	int len=strlen(keyword);
	for(i=0,p=root;i<len;i++)
	{
		v=keyword[i]-'a';
		if(p->next[v]==NULL)
		{
			q=(struct Node *)malloc(sizeof(Node));
			Init(q);
			p->next[v]=q;//结点链接
		}
		p=p->next[v];//指针移动到下一个结点
	}
	p->cnt++;//单词最后一个结点cnt++,代表一个单词
}

构建完成后的效果如下图:

  • 构建失败指针

  • 构建失败指针是AC自动机的关键所在,可以说,若没有失败指针,所谓的AC自动机只不过是Trie树而已。
  • 失败指针原理:
  • 构建失败指针,使当前字符失配时跳转到另一段从root开始每一个字符都与当前已匹配字符段某一个后缀完全相同且长度最大的位置继续匹配,如同KMP算法一样,AC自动机在匹配时如果当前字符串匹配失败,那么利用失配指针进行跳转。由此可知如果跳转,跳转后的串的前缀必为跳转前的模式串的后缀,并且跳转的新位置的深度(匹配字符个数)一定小于跳之前的节点(跳转后匹配字符数不可能大于跳转前,否则无法保证跳转后的序列的前缀与跳转前的序列的后缀匹配)。所以可以利用BFS在Trie上进行失败指针求解。
  • 失败指针利用:
  • 如果当前指针在某一字符s[m+1]处失配,即(p->next[s[m+1]]==NULL),则说明没有单词s[1...m+1]存在,此时,如果当前指针的失配指针指向root,则说明当前序列的任何后缀不是是某个单词的前缀,如果指针的失配指针不指向root,则说明当前序列s[i...m]是某一单词的前缀,于是跳转到当前指针的失配指针,以s[i...m]为前缀继续匹配s[m+1]。
  • 对于已经得到的序列s[1...m],由于s[i...m]可能是某单词的后缀,s[1...j]可能是某单词的前缀,所以s[1...m]中可能会出现单词,但是当前指针的位置是确定的,不能移动,我们就需要temp临时指针,令temp=当前指针,然后依次测试s[1...m],s[i...m]是否是单词。
  • >>>简单来说,失败指针的作用就是将主串某一位之前的所有可以与模式串匹配的单词快速在Trie树中找出。

第二步:构建失败指针

  1. 在构造完Tire树之后,接下去的工作就是构造失败指针。构造失败指针的过程概括起来就一句话:设这个节点上的字母为C,沿着它父亲节点的失败指针走,直到走到一个节点,它的子结点中也有字母为C的节点。然后把当前节点的失败指针指向那个字母也为C的儿子。如果一直走到了root都没找到,那就把失败指针指向root。具体操作起来只需要:先把root加入队列(root的失败指针指向自己或者NULL),这以后我们每处理一个点,就把它的所有儿子加入队列。
  2. 观察构造失败指针的流程:对照图来看,首先root的fail指针指向NULL,然后root入队,进入循环。从队列中弹出root,root节点与s,h节点相连,因为它们是第一层的字符,肯定没有比它层数更小的共同前后缀,所以把这2个节点的失败指针指向root,并且先后进入队列,失败指针的指向对应图中的(1),(2)两条虚线;从队列中先弹出h(右边那个),h所连的只有e结点,所以接下来扫描指针指向e节点的父节点h节点的fail指针指向的节点,也就是root,root->next[‘e‘]==NULL,并且root->fail==NULL,说明匹配序列为空,则把节点e的fail指针指向root,对应图中的(3),然后节点e进入队列;从队列中弹出s,s节点与a,h(左边那个)相连,先遍历到a节点,扫描指针指向a节点的父节点s节点的fail指针指向的节点,也就是root,root->next[‘a‘]==NULL,并且root->fail==NULL,说明匹配序列为空,则把节点a的fail指针指向root,对应图中的(4),然后节点a进入队列。接着遍历到h节点,扫描指针指向h节点的父节点s节点的fail指针指向的节点,也就是root,root->next[‘h‘]!=NULL,所以把节点h的fail指针指向右边那个h,对应图中的(5),然后节点h进入队列...由此类推,最终失配指针如图所示。

构建失败指针的代码:

void Build_AC_automation(Node *root)
{
	head=0,tail=0;//队列头、尾指针
	queue[head++]=root;//先将root入队
	while(head!=tail)
	{
		Node *p=NULL;
		Node *temp=queue[tail++];//弹出队头结点
		for(int i=0;i<26;i++)
		{
			if(temp->next[i]!=NULL)//找到实际存在的字符结点
			{ //temp->next[i] 为该结点,temp为其父结点
				if(temp==root)//若是第一层中的字符结点,则把该结点的失败指针指向root
					temp->next[i]->fail=root;
				else
				{
					//依次回溯该节点的父节点的失败指针直到某节点的next[i]与该节点相同,
                	//则把该节点的失败指针指向该next[i]节点;
                	//若回溯到 root 都没有找到,则该节点的失败指针指向 root
					p=temp->fail;//将该结点的父结点的失败指针给p
					while(p!=NULL)
					{
						if(p->next[i]!=NULL)
						{
							temp->next[i]->fail=p->next[i];
							break;
						}
						p=p->fail;
					}
					//让该结点的失败指针也指向root
					if(p==NULL)
						temp->next[i]->fail=root;
				}
				queue[head++]=temp->next[i];//每处理一个结点,都让该结点的所有孩子依次入队
			}
		}
	}
}
  • 为什么上述那个方法是可行的,是可以保证从root到所跳转的位置的那一段字符串长度小于当前匹配到的字符串长度且与当前匹配到的字符串的某一个后缀完全相同且长度最大呢?
    • 显然我们在构建失败指针的时候都是从当前节点的父节点的失败指针出发,由于Trie树将所有单词中相同前缀压缩在了一起,所以所有失败指针都不可能平级跳转(到达另一个与自己深度相同的节点),因为如果平级跳转,很显然跳转所到达的那个节点肯定不是当前匹配到的字符串的后缀的一部分,否则那两个节点会合为一个,所以跳转只能到达比当前深度小的节点,又因为是由当前节点父节点开始的跳转,所以这样就可以保证从root到所跳转到位置的那一段字符串长度小于当前匹配到的字符串长度。另一方面,我们可以类比KMP求NEXT数组时求最大匹配数量的思想,那种思想在AC自动机中的体现就是当构建失败指针时不断地回到之前的跳转位置,然后判断跳转位置的下一个字符是否包含当前字符,如果是就将失败指针与那个跳转位置连接,如果跳转位置指向NULL就说明当前匹配的字符在当前深度之前没有出现过,无法与任何跳转位置匹配,而若是找到了第一个跳转位置的下一个字符包含当前字符的的跳转位置,则必然取到了最大的长度,这是因为其余的当前正在匹配的字符必然在第一个跳转位置的下一个字符包含当前字符的的跳转位置深度之上,而那样的跳转位置就算可以,也不会是最大的(最后一个字符的深度比当前找到的第一个可行的跳转位置的最后一个字符的深度小,串必然更短一些)。
    • 第三步:匹配

      这样就证明了这种方法构建失败指针的可行性。

第三步:匹配

  1. 最后,我们便可以在AC自动机上查找模式串中出现过哪些单词了。匹配过程分两种情况:(1)当前字符匹配,表示从当前节点沿着树边有一条路径可以到达目标字符,此时只需沿该路径走向下一个节点继续匹配即可,目标字符串指针移向下个字符继续匹配;(2)当前字符不匹配,则去当前节点失败指针所指向的字符继续匹配,匹配过程随着指针指向root结束。重复这2个过程中的任意一个,直到模式串走到结尾为止。
  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时,找不到任何匹配,匹配过程结束。
  3. AC自动机时间复杂性为:O(L(T)+max(L(Pi))+m)其中m是模式串的数量

匹配代码:

int query(Node *root)
{ //i为主串指针,p为模式串指针
	int i,v,count=0;
	Node *p=root;
	int len=strlen(s);
	for(i=0;i<len;i++)
	{
		v=s[i]-'a';
		//由失败指针回溯查找,判断s[i]是否存在于Trie树中
		while(p->next[v]==NULL && p!=root)
			p=p->fail;
		p=p->next[v];//找到后p指针指向该结点
		if(p==NULL)//若指针返回为空,则没有找到与之匹配的字符
			p=root;
		Node *temp=p;//匹配该结点后,沿其失败指针回溯,判断其它结点是否匹配
		while(temp!=root)//匹配结束控制
		{
			if(temp->cnt>=0)//判断该结点是否被访问
			{
				count+=temp->cnt;//由于cnt初始化为 0,所以只有cnt>0时才统计了单词的个数
				temp->cnt=-1;//标记已访问过
			}
			else//结点已访问,退出循环
				break;
			temp=temp->fail;//回溯 失败指针 继续寻找下一个满足条件的结点
		}
	}
	return count;
}

本例题的完整模板代码请点击查看博文:http://blog.csdn.net/liu940204/article/details/51345954

暂时的AC自动机的讲解就这么愉快地结束了,未完待续......

时间: 2024-11-03 05:43:36

AC自动机算法及模板的相关文章

AC自动机算法详解

首先简要介绍一下AC自动机:Aho-Corasick automation,该算法在1975年产生于贝尔实验室,是著名的多模匹配算法之一.一个常见的例子就是给出n个单词,再给出一段包含m个字符的文章,让你找出有多少个单词在文章里出现过.要搞懂AC自动机,先得有模式树(字典树)Trie和KMP模式匹配算法的基础知识.AC自动机算法分为3步:构造一棵Trie树,构造失败指针和模式匹配过程.     如果你对KMP算法和了解的话,应该知道KMP算法中的next函数(shift函数或者fail函数)是干

AC自动机算法

AC自动机简介:  首先简要介绍一下AC自动机:Aho-Corasick automation,该算法在1975年产生于贝尔实验室,是著名的多模匹配算法之一.一个常见的例子就是给出n个单词,再给出一段包含m个字符的文章,让你找出有多少个单词在文章里出现过.要搞懂AC自动机,先得有字典树Trie和KMP模式匹配算法的基础知识.KMP算法是单模式串的字符匹配算法,AC自动机是多模式串的字符匹配算法. AC自动机的构造: 1.构造一棵Trie,作为AC自动机的搜索数据结构. 2.构造fail指针,使当

杭电ACM1277——全文检索~~AC自动机算法

题目的意思:给你一篇文章,再给你T个字符串,判断这T个字符串有哪些在文章中出现过. 由于文章很大,普通的方法必定超时,所以需要用 AC自动机算法. AC自动机算法是多模匹配算法之一,主要是用于在一篇文章中,找出给定的N个单词在这篇文章中出现的个数. AC自动机算法,我也是刚刚学习,主要是在建立字典树的基础上,增加了失败指针,提高了匹配的效率.而且最难的是失败指针的建立. 它的优点是:最大限度地减少无谓的字符串比较,查询效率比哈希表高. 对于AC自动机算法,可以参考大神的博客:点击打开链接 里面有

结合双数组Trie的AC自动机算法

结合双数组Trie的AC自动机算法是一种相对比较快的词匹配算法,常见于分词系统用于分词,本文准备用伪代码结合实例的形式来讲解一下该匹配算法的实现原理 构建步骤: 1.Trie树的构建 首先直观感受一下Trie树: 插入模式串 ball.bat.doll.dork.dorm.do.send.sense之后形成的trie树 trie树分三步:插入(构建).查找.删除 插入: 1.输入一个模式串 String 2.如果该模式串已经存在于树中,则跳转 8.如果不存在,跳转3 3.设置当前节点为根节点 4

转载 - AC自动机算法

出处:http://blog.csdn.net/niushuai666/article/details/7002823 AC自动机简介:  首先简要介绍一下AC自动机:Aho-Corasick automation,该算法在1975年产生于贝尔实验室,是著名的多模匹配算法之一.一个常见的例子就是给出n个单词,再给出一段包含m个字符的文章,让你找出有多少个单词在文章里出现过.要搞懂AC自动机,先得有字典树Trie和KMP模式匹配算法的基础知识.KMP算法是单模式串的字符匹配算法,AC自动机是多模式

AC自动机算法学习

KMP+TRIE int val[1000100][31],tot; int tr[1000100]; int fail[1000100]; struct AC_Trie{ void clean(){ tot=0; memset(val,0,sizeof(val)); memset(tr,0,sizeof(tr)); memset(fail,0,sizeof(fail)); } void build(){ queue<int> q; memset(fail,0,sizeof(fail)); w

算法模板——AC自动机

实现功能——输入N,M,提供一个共计N个单词的词典,然后在最后输入的M个字符串中进行多串匹配(关于AC自动机算法,此处不再赘述,详见:Aho-Corasick 多模式匹配算法.AC自动机详解.考虑到有时候字典会相当稀疏,所以引入了chi和bro指针进行优化——其原理比较类似于邻接表,这个东西本身和next数组本质上是一致的,只是chi和bro用于遍历某一节点下的子节点,next用于查询某节点下是否有需要的子节点) 1 type 2 point=^node; 3 node=record 4 ex:

Keywords Search---hdu2222(AC自动机 模板)

题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=2222 一个常见的例子就是给出n个单词,再给出一段包含m个字符的文章,让你找出有多少个单词在文章里出现过: 本题就是最基础的模板:在此之前需要理解kmp和字典树(trie); Trie树有3个基本性质: (1) 根节点不包含字符,除根节点外每一个节点都只包含一个字符: (2) 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串: (3) 每个节点的所有子节点包含的字符都不相同. 1.

HDU 2222 Keywords Search (AC自动机入门 模板)

AC自动机入门 Aho-Corasick automaton,该算法在1975年产生于贝尔实验室,是著名的多模匹配算法之一.学习AC自动机之前得先有Trie树和KMP模式匹配算法的基础. AC自动机算法分为3步:1.构造一棵tire树  2.构造失败指针  3.进行模式匹配 AC自动机的优化:Trie图 Keywords Search Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Other