AC自动机的优化及经典例题

AC自动机是一种用于解决多模式串匹配问题的工具。

模板题:给定n个模式串和1个母串(由小写字母组成),将母串中包含模式串的部分变为"*"号。

判断一个串是不是另一个串的子串,我们首先会想到KMP算法,但KMP算法需要逐个处理每一个模式串,n太大时显然会超时。这时,AC自动机便派上了用场,它的核心也是熟悉的next数组,我们可以把它看做trie树上的KMP。首先,我们把所有模式串加入一棵trie树中(注意,我们要把trie树的根结点设为1,原因下面会说),接着,我们通过bfs求出trie树上每一个结点的next值(next的含义和KMP中没有实质的差别),代码如下。

q[++tail]=1;
for(int i=1;i<=26;i++) son[0][i]=1;
while(tail>head)
{
	tmp=q[++head];
	for(int i=1;i<=26;i++)
		if(son[tmp][i])
		{
			q[++tail]=son[tmp][i],now=next[tmp];
			while(!son[now][i]) now=next[now];
			next[son[tmp][i]]=son[now][i];
		}
}

以上代码的前两行看起来有些奇怪,第一行把trie树的根结点1加入队列,第二行又设置了一个0号结点,并把它的所有子结点都设为1。其实,这样做是为了避免匹配中的一些特殊情况:假设遇到一个trie树上无法找到的字符i,对于任何一个now,都满足son[now][i]=0,当now=0时,next[now]=0,程序就会无限循环。为了避免这样的问题,我们把trie树的根结点设为1,当now=0时,无论i取何值,都满足son[0][i]=1,从而退出循环。建出next数组后,我们开始匹配母串,即让母串在这棵trie树上顺着next数组跑,记一个tmp表示当前到达的结点,但还有一个细节要注意:统计答案时,我们不应只统计tmp所在结点的答案,还应统计每一个tmp所在结点顺着next数组能到达的结点(只要对于每一个tmp,再开一个now=tmp,顺着next数组跑一跑就行了),否则会出现如下情况:

hack数据:

输入:2

gui

u

guigu

输出:***gu(漏掉了u

为什么会出现这样的情况呢?有些模式串可能是其他模式串的子串,所以会被遗漏...代码如下。

gets(s+1),m=strlen(s+1),tmp=1;
for(int i=1;i<=m;i++)
{
	ch=s[i]-96;
	while(!son[tmp][ch]) tmp=next[tmp];
	tmp=son[tmp][ch],now=tmp;
	while(now)
	{
		if(vis[now])
			for(r int j=1;j<=len[now];j++) s[i-j+1]='*';
    //len[now]表示以now结点结尾的模式串的长度,匹配到一个模式串,就要把它在母串中的部分全部变成"*"号
		now=next[now];
	}
}

例题二、BZOJ3940

我做这题时也按照上面记now=tmp的方法,结果超时了好几发,后来仔细看看题目,已经保证了一个模式串不可能为其他模式串的子串,因此根本不需要now这个变量,只需要直接判断vis[tmp]即可。这道题和上一题有一个区别:删掉一个模式串后,两边剩余的母串可能会拼出新的模式串,那应该如何操作呢?我一开始想到开一个to数组表示母串中每一个字符的下一个字符,核心代码如下。

if(vis[tmp]) to[i-dep[tmp]]=i+1,i=1;

结果又WATLE...WA的原因有二。一:每次删除一个模式串,就从头再开始匹配,但tmp并没有变为1。二:如果删除一个模式串后,两边的母串会拼成一个新的模式串,to数组指向的位置就会出错,因为dep[tmp]没有相应变化。TLE的原因也显而易见:每次删除一个模式串,就从头再开始匹配,造成了极大的时间浪费,其实我们只要从被删模式串的前一位继续匹配即可,但是注意,不能直接把tmp变为1,而要开一个loc数组记录匹配到母串的每一位时tmp所在的位置。解决了TLE的问题,如何解决WA的问题呢?其实只要开一个栈就可以了...栈中的元素不能是字符,而是i,否则继续WA...匹配代码如下。

tmp=1;
for(int i=1;i<=m;i++)
{
	ans[++top]=i,ch=s[i]-96;
	while(!son[tmp][ch]) tmp=next[tmp];
	loc[i]=tmp=son[tmp][ch];
	if(dep[tmp]) top-=dep[tmp],tmp=loc[ans[top]];
}

AC自动机的重要优化:构建trie图。

AC自动机的复杂度是什么?这是个值得思考的问题,for循环中的while看起来十分碍事,事实上,它也的确能被某些特殊数据卡到TLE,有没有什么办法去掉中间的while循环呢?构建trie图即可。什么是trie图?代码如下。

q[++tail]=1;
for(int i=1;i<=26;i++) son[0][i]=1;
while(tail>head)
{
	tmp=q[++head];
	for(r int i=1;i<=26;i++)
		if(!son[tmp][i]) son[tmp][i]=son[next[tmp]][i];
		else
		{
			q[++tail]=son[tmp][i];
			next[son[tmp][i]]=son[next[tmp]][i];
		} 
}

构建trie图的代码和构建AC自动机有何区别?我们首先会发现代码中多了对不存在子的结点情况的判断,为什么这样是对的?如果匹配时,母串的字符不是当前结点的子结点,就需要通过next数组往上跳,跳到一个有这个字符作为子结点的结点。我们发现,往上跳的操作是大量重复的,很多不同的结点跳过同样一段路径,到达同一个终点,却要被重复计算,为什么我们不能利用记忆化的思想,把它的终点记下来呢?于是,我们直接令son[tmp][i]=son[next[tmp]][i],就愉快地解决了这个问题。这种被补成完全k叉树(k为字符集大小)的trie树,就是之前说的trie图。因为每一个结点都有子结点,所以while循环就根本不会开始。因此,在建好trie图之后,next数组就失去了作用,我们直接让母串在trie图上一直走向子结点即可,这样就可以删掉while循环了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值