刚接触AC自动机的时候觉得应该不难,可是~可是到了后来才觉得亚历山大,在这里,先捡最简单的来说吧,对AC自动机的认识及AC自动机模版。
首先,我们需要对trie树和KMP有初步了解,那么我们先回顾一下:
KMP
首先这个匹配算法,主要思想就是要充分利用上一次的匹配结果,找到匹配失败时,模式串可以向前移动的最大距离。这个最大距离,必须要保证不会错过可能的匹配位置,因此这个最大距离实际上就是模式串当前匹配位置的next数组值。也就是max{Aj 是 Pi 的后缀 j < i},pi表示字符串A[1...i],Aj表示A[1...j]。模式串的next数组计算则是一个自匹配的过程。也是利用已有值next[1...i-1]计算next[i]的过程。我们可以看到,如果A[i] = A[next[i-1]+1] 那么next[i] = next[i-1],否则,就可以将模式串继续前移了。
trie树
首先trie树实际上就是一些字符串组成的一个字符查找树,边由代表组成字符串的字符代表,这样我们就可以在O(len(str))时间里判断某个字符串是否属于该集合。trie树的节点内分支可以用链表也可以用数组实现,各有优劣。
简单的trie树每条边由一个字符代表,但是为了节省空间,可以让边代表一段字符,这就是trie的压缩表示。通过压缩表示可以使得trie的空间复杂度与单词节点数目成正比。
可是,若是多模式匹配呢?如果是一堆单词问是否在一篇文章中出现过,那么用KMP显然很耗时,而用trie树又解决不了问题,于是,我们讲kmp和trie图结合起来,形成可如今的AC自动机。
最开始我还以为是可以自动AC的一种机制,谁知,这竟然是两个人名字(Aho-Corasick)的缩写,这叫我等屌丝情何以堪?
废话少说,ac自动机,可以看成是kmp在多字符串情况下扩展形式,可以用来处理多模式串匹配。只要为这些模式串建立一个trie树,然后再为每个节点建立一个失败指针,也就是类似与kmp的next函数,让我们知道如果匹配失败,可以再从哪个位置重新开始匹配。
所以,步骤:
1、建立trie树
2、构造fail指针
3、遍历文章
应该还记得,在kmp构造next数组时,我们是从前往后构造,即先构造1...i-1,然后再利用它们计算next[i],这里也是类似。不过这个先后,是通过bfs的顺序来体现的。AC自动机的失败指针具有同样的功能,也就是说当我们的模式串在Tire上进行匹配时,如果与当前节点的关键字不能继续匹配的时候,就应该去当前节点的失败指针所指向的节点继续进行匹配。而从根到这个失败指针指向的节点组成的字符串,实际上就是跟当前节点的后缀的匹配最长的字符串。
构造fail指针的过程:
如同KMP中模式串得自我匹配一样.从根节点开始,对于每个结点:设该结点上得字符为k,沿着其父亲结点得失败指针走,直到到达根节点或者当前失败指针结点也存在字符为k得儿子结点,那么前一种情况当然是把失败指针设为根节点,而后一种情况则设为当前失败指针结点得字符为k得儿子结点.
我们也可以动手操作一下,如果我们的ac自动机只包含一个模式串,这个过程实际上就是kmp的计算过程。
接下来要做的就是进行文本匹配:
首先,Trie-(模式串集合)中有一个指针p1指向head,而文本串中有一个指针p2指向串头。下面的操作和KMP很类似:如果设k为p2指向的字母 ,而在Trie中p1指向的节点存在字符为k的儿子,那么p2++,p1则改为指向那个字符为k的儿子,否则p1顺着当前节点的失败指针向上找,直到p1存在一个字符为k的儿子,或者p1指向根结点。如果p1路过一个标记为模式串终点的结点,那么以这个点为终点的的模式串就已经匹配过了.或者如果p1所在的点可以顺着失败指针走到一个模式串的终结结点,那么以那个结点结尾的模式串也已经匹配过了。
好了,下面上图<图是Z神画的>:
嗯,那么fail指针如何用呢?通过这棵树搜索时,若发现当前点的孩子中没有当前所判断的字符...就看当前点的Fail点的孩子有无,一直往上Fail直到找到一个点其孩子有所判断的字符或者说找到了根都找不到符合的,就只能说这个字符不属于任何一个单词,只能继续搜下一个字符。
好,来说说本题,题意就是给定N个单词和一个文章,问在文章中出现过几个已给定的单词<出现过就不算了>。
#include<iostream>
#include<stdio.h>
#include<queue>
using namespace std;
struct node{
node *fail,*s[27];
int w;
}*head;
int t,n,i,j,sum;
char temp[51],str[1000001];
queue<node*> myqueue;
node *getfail(node *p,int k){
if (p->s[k]!=NULL) return p->s[k];
else
if (p==head) return head;
else return getfail(p->fail,k);
}
void built_trie(){
node *root=head;
node *tep;
for (j=0;j<strlen(temp);j++){
if (root->s[temp[j]-'a']==NULL){
tep=new node;
for (int k=0;k<26;k++)
tep->s[k]=NULL;
tep->w=0;
tep->fail=head;
root->s[temp[j]-'a']=tep;
}
root=root->s[temp[j]-'a'];
if (j==strlen(temp)-1) root->w+=1;
}
return ;
}
void built_ac(){
node *root;
while (!myqueue.empty()) myqueue.pop();
myqueue.push(head);
while (!myqueue.empty()){//构造fail指针用BFS的方法扩展
root=myqueue.front();
myqueue.pop();
for (j=0;j<26;j++)
if (root->s[j]!=NULL){
myqueue.push(root->s[j]);
if (root==head) root->s[j]->fail=head;
else root->s[j]->fail=getfail(root->fail,j);
}
}
return ;
}
void find(){
int len=strlen(str);
node* tep;
node *root=head;
for (j=0;j<len;j++){
while (root->s[str[j]-'a']==NULL && root!=head) root=root->fail;
root=(root->s[str[j]-'a']==NULL)?head:root->s[str[j]-'a'];
tep=root;
while (tep!=head){//如果单词A出现过了,那么与他具有相同前缀的单词也都出现过了
sum+=tep->w;
tep->w=0;
tep=tep->fail;
}
}
return ;
}
int main(){
scanf("%d",&t);
while (t--){
sum=0;
head=new node;
for (i=0;i<26;i++)
head->s[i]=NULL;
head->fail=head;
head->w=0;
scanf("%d",&n);
for (i=0;i<n;i++){
scanf("%s",temp);
built_trie();
}
built_ac();
scanf("%s",str);
find();
printf("%d\n",sum);
}
return 0;
}