AC自动机的实现原理

       最近学习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;
}


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值