AC自动机讲解
序言:经常听别人说AC自动机,觉得ac自动机是个很神奇,很高深,很难的算法,学完之后发现,ac自动机确实很神奇,很高深,但是却并不难,只要知道怎么构建失败指针就基本初步掌握了。
先了解下AC自动机:首先简要介绍一下AC自动机:Aho-Corasick
automation,该算法在1975年产生于贝尔实验室,是著名的多模匹配算法之一。一个常见的例子就是给出n个单词,再给出一段包含m个字符的文章,让你找出有多少个单词在文章里出现过。要搞懂AC自动机,先得有模式树(字典树)Trie和KMP模式匹配算法的基础知识。KMP算法是单模式串的字符匹配算法,AC自动机是多模式串的字符匹配算法。AC自动机和字典树的关系比较大,所以先来简单的了解下字典树Trie。字典树又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。
简而言之:字典树就是像平时使用的字典一样的,我们把所有的单词编排入一个字典里面,当我们查找单词的时候,我们首先看单词首字母,进入首字母所再的树枝,然后看第二个字母,再进入相应的树枝,假如该单词再字典树中存在,那么我们只用花费单词长度的时间查询到这个单词。
正文开始:
AC自动机算法分为3步:1.构造一棵Trie树(字典树),2.构造失败指针和 3.模式匹配过程。
首先介绍下结构体:typedef struct A {
int cent;//记录是否为尾节点
A *next[26];//子节点指针
A *fail;//失败指针
A(){//初始化
cent=0,ms(next),fail=NULL;
}
}node;
1.构造一棵Trie树(字典树)字典树的构建过程是这样的,当要插入许多单词的时候,我们要从前往后遍历整个字符串,当我们发现当前要插入的字符其节点再先前已经建成,我们直接去考虑下一个字符即可,当我们发现当前要插入的字符没有再其前一个字符所形成的树下没有自己的节点,我们就要创建一个新节点来表示这个字符,接下往下遍历其他的字符。然后重复上述操作。
假设我们有下面的单词,she , he ,say, her, shr ,我们要构建如下一棵字典树
插入一个子串的代码如下(如果需要插入多个子串,重复调用即可):void Build_Tiree(string s)//建字典树{
node* q=root;
for(int i=0;i
int k=s[i]-'a';
if(q->next[k]==NULL)//如果没有分配空间
q->next[k]=new node();//创建空间
q=q->next[k];
}
q->cent++;
}
2.构造失败指针 (重点):1.在构造完Tire树之后,接下去的工作就是构造失败指针。构造失败指针的过程概括起来就一句话:设这个节点上的字母为C,沿着它父亲节点的失败指针走,直到走到一个节点,它的子结点中也有字母为C的节点。然后把当前节点的失败指针指向那个字母也为C的儿子。如果一直走到了root都没找到,那就把失败指针指向root。
具体操作起来只需要:先把root加入队列(root的失败指针指向自己或者NULL),这以后我们每处理一个点,就把它的所有儿子加入队列。2.观察构造失败指针的流程:对照图来看。
(1) 首先root的fail指针指向NULL,然后root入队,进入循环。
(2) 从队列中弹出root,root节点与h、s节点相连,因为它们是第一层的字符,肯定没有比它层数更小的共同前后缀,所以把这2个节点的失败指针指向root,并且先后进入队列,失败指针的指向对应图中的(1),(2)两条红线;
(3) 从队列中先弹出h(左边那个),h所连的只有e结点,所以接下来扫描指针指向e节点的父节点h节点的fail指针指向的节点,也就是root,root->next['e'] == NULL,并且root->fail == NULL,说明匹配序列为空,则把节点e的fail指针指向root,对应图中的蓝色,然后节点e进入队列;
(4)从队列中弹出s,s节点与a,h(左边那个)相连,先遍历到a节点,扫描指针指向a节点的父节点s节点的fail指针指向的节点,也就是root,root->next['a'] == NULL,并且root->fail == NULL,说明匹配序列为空,则把节点a的fail指针指向root,对应图中的蓝色,然后节点a进入队列。
(5)接着遍历到h节点,扫描指针指向h节点的父节点s节点的fail指针指向的节点,也就是root,root->next['h'] != NULL,所以把节点h的fail指针指向右边那个h,对应图中的蓝色,然后节点h进入队列...由此类推,最终失配指针如图所示。
实现代码:void Build_AC_Tiree()//初始化 fail指针{
queueq;
q.push(root);//根结点入队
while(!q.empty())
{
node *re=q.front();//头节点出队
q.pop();//删除
for(int i=0;i<26;i++)//遍历子节点
{
if(re->next[i]!=NULL)//判断,是否存在此子节点
{
if(re==root) //特判是否为根节点
re->next[i]->fail=root;
else
{
node *p=re->fail;
while(p!=NULL)//向上寻找失败指针
{
if(p->next[i]!=NULL)
{
re->next[i]->fail=p->next[i];
break;
}
p=p->fail;
}
if(p==NULL)//为空直接指向root
re->next[i]->fail=root;
}
q.push(re->next[i]);
}
}
}
}
3.模式匹配过程:1.最后,我们便可以在AC自动机上查找模式串中出现过哪些单词了。匹配过程分两种情况:
(1)当前字符匹配,表示从当前节点沿着树边有一条路径可以到达目标字符,此时只需沿该路径走向下一个节点继续匹配即可,目标字符串指针移向下个字符继续匹配;
(2)当前字符不匹配,则去当前节点失败指针所指向的字符继续匹配,匹配过程随着指针指向root结束。重复这2个过程中的任意一个,直到模式串走到结尾为止。
2.对例子来说:其中模式串为yasherhs。
(1) 对于i=0,1。Trie中没有对应的路径,故不做任何操作;
(2) i=2,3,4时,指针p走到左下节点e。因为节点e的cent信息为1,所以ans+1,并且将节点e的cent值设置为-1,表示改单词已经出现过了,防止重复计数,最后temp指向e节点的失败指针所指向的节点继续查找,以此类推,最后temp指向root,退出while循环,这个过程中ans增加了2。表示找到了2个单词she和he。
(3)当i=5时,程序进入第5行,p指向其失败指针的节点,也就是右边那个e节点,随后在第6行指向r节点,r节点的cent值为1,从而ans+1,循环直到temp指向root为止。最后i=6,7时,找不到任何匹配,匹配过程结束。
代码实现:int query(string s){
int ans=0;
node *p=root;
for(int i=0;i
{
int k=s[i]-'a';
while(p->next[k]==NULL&&p!=root)//如果p子节点k不存在,那表明匹配失败
p=p->fail;//找失败节点
p=p->next[k];//指向下一个节点
if(p==NULL)//特判下父节点是不是根节点
p=root;
node *temp=p;
while(temp!=root&&temp->cent>=0)//这个是找这个子串的其他子串
{
ans+=temp->cent;
temp->cent=-1;
temp=temp->fail;
}
}
return ans;
}
最后HDU 2222的AC代码如下:
#include
#define inf 0x3f3f3f3f
#define ll long long
#define sscc ios::sync_with_stdio(false);
#define ms(a) memset(a,0,sizeof(a))
using namespace std;
typedef struct A {
int cent;//记录是否为尾节点
A *next[26];//子节点指针
A *fail;//失败指针
A(){//初始化
cent=0,ms(next),fail=NULL;
}
}node;
node *root;
void Build_Tiree(string s)//建字典树{
node* q=root;
for(int i=0;i
int k=s[i]-'a';
if(q->next[k]==NULL)//如果没有分配空间
q->next[k]=new node();//创建空间
q=q->next[k];
}
q->cent++;
}
void Build_AC_Tiree()//初始化 fail指针{
queueq;
q.push(root);//根结点入队
while(!q.empty())
{
node *re=q.front();//头节点出队
q.pop();//删除
for(int i=0;i<26;i++)//遍历子节点
{
if(re->next[i]!=NULL)//判断,是否存在此子节点
{
if(re==root) //特判是否为根节点
re->next[i]->fail=root;
else
{
node *p=re->fail;
while(p!=NULL)//向上寻找失败失败指针
{
if(p->next[i]!=NULL)
{
re->next[i]->fail=p->next[i];
break;
}
p=p->fail;
}
if(p==NULL)//为空直接指向root
re->next[i]->fail=root;
}
q.push(re->next[i]);
}
}
}
}
int query(string s){
int ans=0;
node *p=root;
for(int i=0;i
{
int k=s[i]-'a';
while(p->next[k]==NULL&&p!=root)//如果p子节点k不存在,那表明匹配失败
p=p->fail;//找失败节点
p=p->next[k];//指向下一个节点
if(p==NULL)//特判下父节点是不是根节点
p=root;
node *temp=p;
while(temp!=root&&temp->cent>=0)//这个是找这个子串的其他子串
{
ans+=temp->cent;
temp->cent=-1;
temp=temp->fail;
}
}
return ans;
}
int main(){
sscc;//不开这个会超时
int t,k;
string s;
cin>>t;
while(t--)
{
cin>>k;
root=new node();
for(int i=0;i
string c;
cin>>c;
Build_Tiree(c);
}
Build_AC_Tiree();//构建失败指针
cin>>s;
cout<
}
return 0;
}