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]下的最长前缀的最长后缀,该操作便是(之前看讲解对此都是含糊不清的)。
贴出代码(hdu2222):
建立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; }
相关文章推荐
- 关于base64编码的原理及实现
- WebSocket 的原理和Client的实现
- java集合框架学习—HashSet的实现原理
- C/C++知识要点5——智能指针原理及自定义实现
- 事务实现原理-回滚原理
- 拓扑排序的原理&&实现
- 单步调试理解webpack里通过require加载nodejs原生模块实现原理
- ftrace:event的实现原理和使用方法
- 仿多页面滑动切换时背景指示图(如TAB文字下边的白条等)的动画实现原理,例PagerSlidingTabStrip
- 图像Ostu二值化原理及matlab实现代码
- Spring(八)编码剖析@Resource注解的实现原理
- Java断点续传实现原理很简单
- 119_容器_自定义实现迭代器_深入迭代器_迭代器原理_面向对象实现
- 数据库隔离级别 及 其实现原理
- HashMap的实现原理
- 负载均衡lvs、keepalived简介,DR实现原理
- 平衡二叉树的实现原理
- 《深入理解mybatis原理(六)》 MyBatis缓存机制的设计与实现如何细粒度地控制你的MyBatis二级缓存
- Java设计模式——策略模式实现及原理
- 实现quartz定时器及quartz定时器原理介绍