您的位置:首页 > 其它

AC自动机的实现原理

2012-12-25 17:21 148 查看
最近学习AC自动机,看了不少讲解AC自动机的文章,几乎都是在讲如何操作。估计不少人学习时像我一样在想AC自动机算法为什么能实现多模式串匹配操作。如下是我的思考成果,如有漏洞,欢迎指正。

建立trie树比较容易,构造fail指针其实是同样的匹配过程,只要理解query()也就都明白了,下面主要来说说query()是如何完整地查找出所有的模式串的。

对于给定的长字符串,查找有多少模式串在里面出现过。query函数依次读入长字符串里的字符,而匹配某一模式串操作是在query函数读入长字符串的某一字符时发生的。query依次读入字符,不受其他操作影响(无论有没有发生匹配query都老老实实地一个接一个地读串)。每读入长字符串中的一个字符str[i],便需要求这样一个子串,设为s[i](即str(k...i)串),满足如下关系:

该子串是某一模式串的前缀,且是所有模式串中的最长前缀, 即不存在某字符串的前缀为str(k1...i),k1<k。以下满足该关系的子 串均简称为最长前缀(注意指的是模式串的最长前缀,不要混淆)。
现在要做的是每读入一个str[i],求出它的最长前缀(s[i])。以下是求s[i]的方法:

s[i]=s[i-1]+str[i]-------------------->>>>当前正在匹配的模式串的下一个字符==str[i](显然s[i-1]表示上一状态即扫描到str[i-1]的最长前缀);

s[i]=houzhui(houzhui...(houzhui(s(i-1))))+str[i]---------------->>>>当前正在匹配的模式串的下一个字符!=str[i]

(houzhui()函数取的串满足如下条件1当前正在匹配串的后缀;2其他模式串前缀;3满足条件1、2的最长的串。描述起来很费劲,但你观察已经创建好的trie树和fail指针的特性,发现所谓houzhui()操作是水到渠成的(思考一下,其实trie树有很多隐含特性的)。仅需第75、76行代码。显然,取后缀操作是有截止条件的,截止条件就是当取得的后缀(设为L(j...k))是某字符串(设为L1(1...m))的前缀,且元素L1(k-j+1)==str[i],那么L(j...k)+str[i]便是我们所求的s[i]。)
这个看起来像不像DP中的递推关系式?把每一步求解str[i]看做一个状态,每一步str[i]最长前缀s[i]的求解依赖于上一状态str[i-1]的解,这应该就是AC自动机中的DP思想。

求解出每一状态的最长前缀还远没有结束,现在我们知道了当前状态下(即当前str[i]下)的最长前缀和str[i]这个字符,要求的是在这个str[i]下发生匹配的模式串。现在做如下讨论:

若某一模式串在str[i]状态下被匹配,则该模式串的末尾字符==str[i],且之前的字符是str[i]的最长前缀的某一后缀。
下面就查找满足上述关系的模式串进行匹配操作,也就是依次查找str[i]下的最长前缀的最长后缀,该操作便是(之前看讲解对此都是含糊不清的)。

while(temp!=root&&temp->count!=-1){
cnt+=temp->count;
temp->count=-1;
temp=temp->fail;
}
看是否有模式串恰好是那个str[i]下的最长前缀的后缀。若有,则该串被匹配,不要停,继续找后缀,直到后缀为0,即fail指向了root。(至于为什么继续找应该不需要解释了)此处查找操作再次用到了fail指针,fail指针的作用就是帮助我们找后缀,当然,准确地说是同样出现在模式串中的后缀。

贴出代码(hdu2222):

#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
struct node{
node *fail;
node *next[26];
int count;
}*q[500001];   //用作建trie树时广搜的队列
char keyword[51];
char str[1000001];
int head,tail;
void insert(char str[],node *root){
node *p=root;
int i=0,cur;
while(str[i]){
cur=str[i]-'a';
if(p->next[cur]==NULL)
p->next[cur]=new node();
p=p->next[cur];
i++;
}
p->count++;
}
void build(node *root){
int i;
root->fail=NULL;
q[head++]=root;  //广搜队列
while(head!=tail){
node *temp=q[tail++];
node *p=NULL;
for(i=0;i<26;i++){
if(temp->next[i]!=NULL){
if(temp==root)
temp->next[i]->fail=root;
else{
p=temp->fail;
while(p!=NULL){
if(p->next[i]!=NULL){
temp->next[i]->fail=p->next[i];
break;
}
p=p->fail;
}
if(p==NULL)
temp->next[i]->fail=root;
}
q[head++]=temp->next[i];   //入队列
}
}
}
}
int query(node *root){
int i=0,cnt=0,cur;
node *p=root;
while(str[i]){
cur=str[i]-'a';
while(p->next[cur]==NULL&&p!=root)
p=p->fail;
p=p->next[cur];
p=(p==NULL)?root:p;
node *temp=p;
while(temp!=root&&temp->count!=-1){   //此操作见上述讲解
cnt+=temp->count;
temp->count=-1;
temp=temp->fail;
}
i++;
}
return cnt;
}
int main(){
int t,n;
scanf("%d",&t);
while(t--){
head=tail=0;
node *root=new node();
scanf("%d",&n);
while(n--){
scanf("%s",keyword);
insert(keyword,root);
}
build(root);
scanf("%s",str);
printf("%d\n",query(root));
}
return 0;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: