自动机是一种用于解决多模式串匹配问题的工具。
模板题:给定个模式串和个母串(由小写字母组成),将母串中包含模式串的部分变为号。
判断一个串是不是另一个串的子串,我们首先会想到算法,但算法需要逐个处理每一个模式串,太大时显然会超时。这时,自动机便派上了用场,它的核心也是熟悉的数组,我们可以把它看做树上的。首先,我们把所有模式串加入一棵树中(注意,我们要把树的根结点设为,原因下面会说),接着,我们通过求出树上每一个结点的值(的含义和中没有实质的差别),代码如下。
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];
}
}
以上代码的前两行看起来有些奇怪,第一行把树的根结点加入队列,第二行又设置了一个号结点,并把它的所有子结点都设为。其实,这样做是为了避免匹配中的一些特殊情况:假设遇到一个树上无法找到的字符,对于任何一个,都满足,当时,,程序就会无限循环。为了避免这样的问题,我们把树的根结点设为,当时,无论取何值,都满足,从而退出循环。建出数组后,我们开始匹配母串,即让母串在这棵树上顺着数组跑,记一个表示当前到达的结点,但还有一个细节要注意:统计答案时,我们不应只统计所在结点的答案,还应统计每一个所在结点顺着数组能到达的结点(只要对于每一个,再开一个,顺着数组跑一跑就行了),否则会出现如下情况:
hack数据:
输入:
输出:(漏掉了)
为什么会出现这样的情况呢?有些模式串可能是其他模式串的子串,所以会被遗漏...代码如下。
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
我做这题时也按照上面记的方法,结果超时了好几发,后来仔细看看题目,已经保证了一个模式串不可能为其他模式串的子串,因此根本不需要这个变量,只需要直接判断即可。这道题和上一题有一个区别:删掉一个模式串后,两边剩余的母串可能会拼出新的模式串,那应该如何操作呢?我一开始想到开一个数组表示母串中每一个字符的下一个字符,核心代码如下。
if(vis[tmp]) to[i-dep[tmp]]=i+1,i=1;
结果又又...的原因有二。一:每次删除一个模式串,就从头再开始匹配,但并没有变为。二:如果删除一个模式串后,两边的母串会拼成一个新的模式串,数组指向的位置就会出错,因为没有相应变化。的原因也显而易见:每次删除一个模式串,就从头再开始匹配,造成了极大的时间浪费,其实我们只要从被删模式串的前一位继续匹配即可,但是注意,不能直接把变为,而要开一个数组记录匹配到母串的每一位时所在的位置。解决了的问题,如何解决的问题呢?其实只要开一个栈就可以了...栈中的元素不能是字符,而是,否则继续...匹配代码如下。
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]];
}
自动机的重要优化:构建图。
自动机的复杂度是什么?这是个值得思考的问题,循环中的看起来十分碍事,事实上,它也的确能被某些特殊数据卡到,有没有什么办法去掉中间的循环呢?构建图即可。什么是图?代码如下。
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];
}
}
构建图的代码和构建自动机有何区别?我们首先会发现代码中多了对不存在子的结点情况的判断,为什么这样是对的?如果匹配时,母串的字符不是当前结点的子结点,就需要通过数组往上跳,跳到一个有这个字符作为子结点的结点。我们发现,往上跳的操作是大量重复的,很多不同的结点跳过同样一段路径,到达同一个终点,却要被重复计算,为什么我们不能利用记忆化的思想,把它的终点记下来呢?于是,我们直接令,就愉快地解决了这个问题。这种被补成完全叉树(为字符集大小)的树,就是之前说的图。因为每一个结点都有子结点,所以循环就根本不会开始。因此,在建好图之后,数组就失去了作用,我们直接让母串在图上一直走向子结点即可,这样就可以删掉循环了。