AC自动机原理说明

来源:互联网 发布:淘宝店铺最怕什么 编辑:程序博客网 时间:2024/05/17 07:51

1. AC自动机的功能:

用于多模匹配,所谓多模匹配,就是给定一个带匹配的字符串string,给定一个字典dictionary,dictionary中有多个字符串{ str1,str2, str3 … } 多模匹配就是要得到string字符串中出现了dictionary的哪些字符,且这些字符出现在了string中的哪个位置。

 

2. AC自动机的原理:

AC自动机的难点在于构建一个DFA(确定状态的有限状态自动机)。构建这个自动机分为两步:

1.      根据dictionary构建一棵前缀树trieTree。

2.      在对这棵trieTree进行BFS广度优先遍历的同时,为这棵树的节点增加边与fail指针。

 

2.1介绍什么是trieTree(前缀树)

         前缀树是一种存储单词的数据结构,从树根遍历到每一片树叶(或者某些中间结点)都是一个单词,两个单词如果有相同的前缀,那么在这棵树上,从根节点到这个相同的前缀结束之前,这两个单词所对应的路径是重叠的。


上图中,从根节点到每个红色的节点经过的路径上的字母组成了一个字典中的单词。

 

 

 

 

以下是trieTree节点的结构:

typedef struct trieNode {

         trieNode*next[KIND]; //初始化都为NULL ,该节点的孩子

         trieNode *fail;

         char value[50];//存放根节点到当前节点的路径上的值

         int finalSig; //表明是否是某段字符串的最后一个值

} trieNode; //trie树的节点

2.2 如何在广度优先搜索的基础上为trieTree加边,同时增加fail指针,形成一个DFA

1.      对trieTree根节点的直接孩子做特殊处理:

 

For( int i=0; i<KIND;++i ){ //KIND是字母的种类,一般是26

         if( root->next[i] == NULL ){ //因为字典里的单词的开头字母可能没有覆盖26个字母,所以是NULL

                  root->next[i] = root;

} else{

         Root->next[i]->fail= root; //trieTree根节点的直接孩子的fail指针指向root节点

}

}

 

2.      对trieTree进行广度优先遍历(层序遍历)

设每次处理的节点为now。

 

If( now->next[i] ==NULL ){

         now->next[i] = now->fail->next[i];

} else{

         now->next[i]->fail = now->fail->next[i];

}

2.3 如何在这个DFA上进行多模匹配

匹配的方法是,有三个指针,str_ptr指向string,now_ptr随着str_ptr在DFA上移动,每当now_ptr移动到一个节点,就开启一个循环:

 

temp_ptr = now_ptr

while( temp_ptr != root){

         //如果temp_ptr指向的当前节点是一个单词的终节点,输出这个单词

         temp_ptr = temp_ptr -> fail  //temp_ptr沿着fail指针跳跃

}

2.4 这样做为什么就可以进行多模匹配,不会漏掉某个隐藏在string中的某个dictionary字符串

1.      为什么通过fail指针跳转到的节点(记为j),从根到该跳转节点j之间路径对应的单词已经匹配成功?

首先要知道一个条件:fail指针指向的节点值要么和自己相同,要么指向root。在此基础上看下面的叙述。

因为:假设原节点为a,跳转后的节点为A。两个节点之间通过fail指针相连。所以a=A,或者A是树根节点root。

原节点a的父节点为Pa,跳转节点A的父节点是PA,根据fail指针的生成过程,Pa与PA之间也是由fail指针相连的。 所以Pa=PA,或者PA是树根节点root。

以此类推,我们就可以知道命题是成立的(更规范的证明可以用数学归纳法)。

 

相对于原来的trieTree,构建DFA新加入了很多边来把原来节点中next[]数组里的NULL指针填补上,这些新加入的边我称做jump。Jump边的加入过程通过观察源代码:

If( now->next[i] ==NULL ){

         now->next[i] = now->fail->next[i]; //添加jump边(指针)

} else{

         now->next[i]->fail = now->fail->next[i]; //添加fail边(指针)

}

Jump边感觉和fail边很类似。其实可以根据上面的证明类比得到,通过jump边跳转到的节点,根到跳转节点之间的路径对应的单词也是匹配成功的。

 


 

2.      定理:沿着fail边跳转到的节点所代表的单词是跳转前节点对应单词的最长后缀

证明:

从上到下,第一个是原来的串str1 第二个是沿着fail跳转后的串str2,假设str2不是str1的最长后缀,而是str3,即第三个串。

蓝色部分的fail指针必然指向root,如果存在str3,那么str1黄色部分的fail指针将指向str3的黄色部分,从而影响后面的fail指针的分布,导致沿着str1末尾的fail跳转会到达str3。



同理可以证明jump沿着边跳转也有类似的性质。

 

1.      为什么不会漏掉某个隐藏在string中的dictionary里的字符串

先理解2.3节多模匹配过程。

参考2.3节多模匹配的过程,我把DFA中的原来属于trieTree的边叫做trie边,生成DFA过程中用来填补每个节点next[]中的NULL的边叫做jump边,还有每个节点中的fail边。(紫色是引用2.3的多模匹配过程

 

匹配的方法是,有三个指针,str_ptr指向stringnow_ptr随着str_ptrDFA上移动,每当now_ptr移动到一个节点,就开启一个循环:

temp_ptr = now_ptr

while( temp_ptr !=root ){

         //如果temp_ptr指向的当前节点是一个单词的终节点,输出这个单词

         temp_ptr = temp_ptr -> fail  //temp_ptr沿着fail指针跳跃

}

 

now_ptr指针随着str_ptr指针一同运动,now_ptr指针只走DFA中的trie边和jump边,并不走fail边。

str_ptr指向了在string中当前匹配的末位置,为了更好的说明匹配过程,再增加一个虚拟的string中的指针begin_ptr,这个指针指向当前可能匹配成功的单词在string中的开始位置(初始状态下,begin_ptr就在string的开头)。

 

先讨论now_ptr运动的过程:

now_ptr在trie边上运动的时候,string中的指针只是str_ptr在一起运动,begin_ptr并不动。当now_ptr通过jump边跳到一个节点后,这时begin_ptr就相当于向右移动了一段距离,变换了当前可能匹配成功的单词。详见下图:


黄色部分是原来认为可能匹配成功的单词(的前缀),蓝色部分是jump边跳跃的部分在string上的体现,红色部分是当前认为可能匹配成功的单词(的前缀),绿色部分是begin_ptr跳跃的一段距离。由于沿着jump边跳转,切换的串是跳转前节点对应单词的最长后缀,所以不会遗漏。

 

再看看temp_ptr的运动过程

每当now_ptr到达一个节点,相当于确定了一个可能成功匹配的单词的前缀PREFIX,temp_ptr就从now_ptr开始,顺着fail边不断向后找,直到找到root,这一在DFA上的过程对应到string上就是不断地更换PREFIX的后缀,试图找到匹配的单词。由于沿着fail边跳转,切换的串是跳转前节点对应单词的最长后缀,所以不会遗漏。

 




0 0
原创粉丝点击