AC自动机

来源:互联网 发布:淘宝直通车几心可以开 编辑:程序博客网 时间:2024/05/16 13:48

AC自动机

          在多模式匹配问题中,我们需要处理的是这种类型的问题:定位模式集合P={P1,....Pk}在目标集合T={1.....m}中出现的次数。
令n=(|p1|+|p2|+|p2|.........+|pk|)
          那么多模式匹配可以在O(|P1| + m + ··· + |Pk| + m) = O(n + km)时间复杂性内完成,(通过调用线性模式匹配算法k次求得结果,
常见线性匹配算法KMP,BM等等)。
          Aho-Corasick算法是一个经典的解决多模式匹配的方法,它的时间复杂性是O(n + m + z),其中z是模式串在目标串中出现的次数。
Aho-Corasick算法是基于字典树或者称为关键字树的一种算法。

字典树

来自维基百科的定义:
计算机科学中,trie,又称前缀树字典树,是一种有序,用于保存关联数组,其中的键通常是字符串。与二叉查找树不同,
键不是直接保存在节点中,而是由节点在树中的位置决定。一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,
而根节点对应空字符串。一般情况下,不是所有的节点都有对应的值,只有叶子节点和部分内部节点所对应的键才有相关的值。
一棵由模式串集合构造出来的字典树T有如下特点:
         1.T中的每一条边都被一个字符标志;
         2.从一个结点发出的任意两条边的标志都不同;
字典树(Trie Tree)结点定义为:
struct node{node *next[26];int cnt;node():pre(NULL),cnt(0){memset(next,NULL,sizeof next);}~node(){for (int i=0; i < 26; ++i)delete next[i];}};
用插入模式串的方法建立字典树的操作为:
void Insert(node *root,char *s){int i,j;for (i=0; s[i]; ++i){j = s[i]-'a';if (root->next[j]==NULL)root->next[j] = new node;root = root->next[j];}++root->cnt;}
字典树的查找操作也比较简单。
int Search(node* root, char *s){node* cur = root;int i,j;for(i=0; s[i]; ++i){j = str[i]-'a';if (cur->next[j]==NULL)break;cur=cur->next[j];}return cur->cnt;}

KMP的next数组的构造

Aho-Corasick算法的核心思想就是在字典树中加上KMP的next数组的思想,让我们回忆一下,KMP的next数组是如何工作的。
next[]保存的是以pattern[i]为末尾的前一最大前缀的位置,如果无公共前缀,则next[i]=0
    pattern string: abc.......abc....
                                     j             i:next[i]=j
匹配:                                          i
    src string       : abc.......abcd
    pattern string: abc.......abce...
                                                   j:失配
找最大公共前缀:                   i
    src string    : abc.......abcd
    pattern string:            abc.......abce...
                                                j=next[j]
next数组的目标是使得相同的部分abc无需再重复匹配,i下标不需要回溯,从而增加了效率。
AC自动机的pre指针思想如图示:实线表示next指针,虚线表示pre指针。
5号结点和2号结点有公共的前缀{h,e},因此5号结点的pre指针指向2号,即如果匹配至5号结点发生失配,则可以直接跳转至2号结点继续匹配,
而无须重新回到root结点,从而提高了效率。
同理有:

那么,如何从一棵字典树中构造出类似KMP一样的next数组呢?这就是Aho-Corasick算法最核心的地方,我们可以在每一个结点添加一个域,这里记为pre。
然后通过BFS按层次遍历整棵树的时候,实现pre指针的定位,具体见代码注释。
void ConstructPre(node *root)//构造类似KMP中的next数组,这里用pre表示,pre[i]即i失配时向前移动的位置{queue<node*> Q;Q.push(root);node *cur,*p;int i;while (!Q.empty()){cur = Q.front();Q.pop();for (i=0; i < 26; ++i){if (cur->next[i]==NULL)//越过空指针continue;for (p=cur->pre; p; p=p->pre)//从后往前求最大前缀对称串,{if (p->next[i])//找到一个前缀对称串,则记录{cur->next[i]->pre = p->next[i];break;}}if (p==NULL)//如果遍历至root依然找不到对称串,则置为root,类似KMP的next[i]置为0cur->next[i]->pre = root;Q.push(cur->next[i]);}}}
接下来是查找的部分,查找部分相对朴素的字典树查找有了一些区别,当已查找到某一叶子结点时,朴素的做法是回到root结点,重新查找,但是
这里加入了pre指针,我们不需要重新从root开始进行下一次查找,可以直接跳至pre指针指向的结点继续查找,因为它们有公共的前缀,当前结点
不为空的时候,朴素的做法是继续往下找,但是,这里,需要查找从当前结点至root的pre链表中统计记数,因为,公共前缀有可能就是某一模式串。
int Aho_Corasick(node *root,char *s)//很神奇的search{int i,j,sum=0;node *cur=root, *p=NULL;for (i=0; s[i]; ++i){j = s[i]-'a';while (cur != root && cur->next[j]==NULL)//如果找到某一匹配单词了,则回溯找另外匹配的,等等,cur = cur->pre;//为什么不记数呢?注意:因为在前一步已经记过数了,可以单步调试感受if (cur->next[j])cur = cur->next[j];//else cur = root;无须这一步,因为恒有cur==rootfor (p=cur; p!=root && p->count != -1; p=p->pre)//对pre链统计记数{sum += p->count;p->count = -1;}}return sum;}
最后,测试一道模板题目HDU2222
//AC自动机:Tried TREE中加上kmp的next数组思想//http://acm.hdu.edu.cn/showproblem.php?pid=2222#include <string>#include <queue>#include <iostream>using namespace std;struct node{node *next[26];node *pre;int count;//保存的是当前结点的记数node():pre(NULL),count(0){memset(next,NULL,sizeof next);}~node(){for (int i=0; i < 26; ++i){delete next[i];}}};node *root = NULL;void Insert(node *root,char *s)//普通的字典树插入,建立一棵字典树{int i,j;node *p = root;for (i=0; s[i]; ++i){j = s[i]-'a';if (p->next[j]==NULL)p->next[j] = new node;p = p->next[j];}++p->count;}void ConstructPre(node *root)//构造类似KMP中的next数组,这里用pre表示,pre[i]即i失配时向前移动的位置{queue<node*> Q;Q.push(root);node *cur,*p;int i;while (!Q.empty()){cur = Q.front();Q.pop();for (i=0; i < 26; ++i){if (cur->next[i]==NULL)//越过空指针continue;for (p=cur->pre; p; p=p->pre)//从后往前求最大前缀对称串,{if (p->next[i])//找到一个前缀对称串,则记录{cur->next[i]->pre = p->next[i];break;}}if (p==NULL)//如果遍历至root依然找不到对称串,则置为root,类似KMP的next[i]置为0cur->next[i]->pre = root;Q.push(cur->next[i]);}}}int Aho_Corasick(node *root,char *s)//很神奇的search{int i,j,sum=0;node *cur=root, *p=NULL;for (i=0; s[i]; ++i){j = s[i]-'a';while (cur != root && cur->next[j]==NULL)//如果找到某一匹配单词了,则回溯找另外匹配的,等等,cur = cur->pre;//为什么不记数呢?注意:因为在前一步已经记过数了,可以单步调试感受if (cur->next[j])cur = cur->next[j];//else cur = root;无须这一步,因为恒有cur==rootfor (p=cur; p!=root && p->count != -1; p=p->pre)//对pre链统计记数{sum += p->count;p->count = -1;}}return sum;}char s[110];char text[1000010];int main(){#ifndef ONLINE_JUDGEfreopen("2.txt","r",stdin);#endifint T,n;scanf("%d ",&T);while (T--){root = new node;scanf("%d ",&n);while (n--){gets(s);Insert(root,s);}gets(text);ConstructPre(root);printf("%d\n",Aho_Corasick(root,text));delete root;}return 0;}

参考资料

AC自动机-Set Matching and Aho-Corasick Algorithm

0 0
原创粉丝点击