简介
KMP 算法是单模式串的字符匹配算法,AC自动机是多模式串的字符匹配算法。
Trie 树是一种哈希树的变种。
AC自动机算法分为3步:
① 构造一棵Trie树;
② 构造失败指针;
③ 模式匹配过程。
结构体
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<s.length();i++){//将字符串插入
int k=s[i]-'a';
if(q->next[k]==NULL)//如果没有分配空间
q->next[k]=new node();//创建空间
q=q->next[k];
}
q->cent++;
}
2. 构造失败指针
构造失败指针的过程
设这个节点上的字母为C,沿着它父亲节点的fail指针走,直到走到一个节点,它的子节点中也有字母为C的节点。然后把当前节点的fail指针指向那个字母也为C的儿子。如果一直走到了root都没找到,那就把失败指针指向root。
具体操作起来只需要:先把root加入队列(root的失败指针指向自己或者NULL),这以后我们每处理一个点,就把它的所有儿子加入队列。
构造失败指针的流程
① 首先root节点的fail指针指向NULL,然后root入队,进入循环。
② 从队列中弹出root,root节点与h、s节点相连,因为它们是第一层的字符,肯定没有比它层数更小的共同前后缀,所以把这2个节点的fail指针指向root,并且先后进入队列,fail指针的指向对应图中的两条红线;
③ 从队列中弹出h(左边那个),h节点所连的只有e结点,所以接下来扫描指针指向e节点的父节点h节点的fail指针指向的节点,也就是root,root->next[‘e’] == NULL,并且root->fail == NULL,说明匹配序列为空,则把节点e的fail指针指向root,对应图中的蓝色,然后节点e进入队列;
④ 从队列中弹出s,s节点与a,h(左边那个)相连,先遍历到a节点,扫描指针指向a节点的父节点s节点的fail指针指向的节点,也就是root,root->next[‘a’] == NULL,并且root->fail == NULL,说明匹配序列为空,则把节点a的fail指针指向root,对应图中的蓝色,然后节点a进入队列。
⑤ 接着遍历到h节点,扫描指针指向h节点的父节点s节点的fail指针指向的节点,也就是root,root->next[‘h’] != NULL,所以把节点h的fail指针指向右边那个h,对应图中的蓝色,然后节点h进入队列…由此类推,最终失配指针如图所示。
void Build_AC_Tiree()//初始化 fail指针
{
queue<node*>q;
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. 模式匹配过程
我们便可以在AC自动机上查找模式串中出现过哪些单词了。匹配过程分两种情况:
① 当前字符匹配,表示从当前节点沿着树边有一条路径可以到达目标字符,此时只需沿该路径走向下一个节点继续匹配即可,目标字符串指针移向下个字符继续匹配;
② 当前字符不匹配,则去当前节点失败指针所指向的字符继续匹配,匹配过程随着指针指向root结束。重复这2个过程中的任意一个,直到模式串走到结尾为止。
对例子来说:其中模式串为yasherhs。
① 对于I=0,1时。Trie中没有对应的路径,故不做任何操作;
② 当 i=2,3,4时,指针p走到左下节点e。因为节点e的cent信息为1,所以ans+1,并且将节点e的cent值设置为-1,表示改单词已经出现过了,防止重复计数,最后temp指向e节点的失败指针所指向的节点继续查找,以此类推,最后temp指向root,退出while循环,这个过程中ans增加了2。表示找到了2个单词she和he。
③ 当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<s.length();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
1
5
she
he
say
shr
her
yasherhs
3