本篇文章主要详细介绍$AC$自动机的$fail$指针:
如果有什么不完善的地方,请联系我$qwq$
前置知识:
1、建议学一下$kmp$算法
2、$Trie$
导入:
AC自动机是用来解决多模板匹配问题的,但是,如果就单纯的把每个模板串拼接在一起,或者单个单个匹配的话,肯定是会超时的,而它的思想是把所有的模式串建立一颗$Trie$,然后用文本串来匹配,那么我们就必须在这颗$Trie$树上进行快速跳转来优化,于是,AC自动机就诞生了
重点:fail指针到底是什么?
我们先来思考一个问题,假如我们按照上面的思想,把每一个模式串建立一颗字典树,那么怎样才能在这个模板串失配后快速跳到下一个有可能成功匹配的字符串来匹配呢?我们举个例子:假设文本串是$bcde$,模式串有两个,分别为$bce$和$cd$,我们为两个模式串建一颗$Trie$,如下图:
我们拿着文本串开始匹配,发现匹配到$2$节点后,就失配了,也就是模式串$bce$失配,那么我们如何快速跳到下一个模式串$cd$来匹配呢?这个时候你可能会说,到$0$节点开始匹配呀,可是这样其实就是重头匹配下一个字符串了,效率并不高,在这里,我们其实可以直接跳到$4$号节点开始匹配,为什么?那是因为我们发现$2$号节点是成功匹配了的,只是不能到$3$号节点去,但是我们发现$2$号节点匹配说明了字符$c$在文本串出现过,那么$c$必定会匹配,所以$0$到$4$节点是怎么样都会匹配的,多举些例子我们会发现,当某个模式串失配后,当另一个模式串的某个前缀等于失配模式串的后缀时,可以得到另一个模式串的前缀肯定会匹配,可以直接跳到前缀继续匹配,也就是下图:
所以,构建$fail$指针其实就是在找最长的与当前失配的模式串后缀相同的与下一个模式串的前缀,那么为什么要找相同的前后缀上面已经说了,就是因为要利用已知信息来加速匹配,那么为什么要最长呢?
其实最长的话就可以保证每个有可能匹配的模式串都匹配到,假设当前节点模式串失配了,就跳最大的前缀,如果又失配,就又到最大前缀的最大前缀,直到没有为止,这样就可以保证全部考虑到。
算法实现(强势图解$qwq$):
先插入代码供下面边模拟边理解
void s_insert(string ss,int order)//构建普通Trie
{
int now=0;
for(int i=0;i
for(int i=0;i<26;i++)
if(ch[0][i])
dui.push(ch[0][i]);
while(!dui.empty())
{
int now=dui.front();
dui.pop();
for(int i=0;i<26;i++)
if(ch[now][i])
{
fail[ch[now][i]]=ch[fail[now]][i];
dui.push(ch[now][i]);
}
else
ch[now][i]=ch[fail[now]][i];
}
}
下面的图片引用了某大佬的图片,
假设文本串为$ushersheishis$,模式串为$i$,$he$,$his$,$she$,$hers$,黄色结点表示当前的结点u,绿色结点表示已经$BFS$遍历完毕的结点,红/橙色的边表示$fail$指针。
$2$号节点的$fail$指针画错了,$fail[2]=0$.
我们重点分析结点$6$的$fail$指针构建:
找到$6$的父节点$5$,$5$的$fail$指针指向$10$,然而$10$结点没有字母$'s'$连出的边;
所以跳到$10$的$fail$指针指向的结点$0$,发现$0$结点有字母$'s'$连出的边,指向$7$结点;
所以$fail[6]=7$.
另外,在构建$fail$指针的同时,我们也对$Trie$中模式串的结尾构建$fail$指针。这样在匹配到结尾后能自动跳转到下一个匹配项。具体见代码实现。
深入优化:
一般我们建立$AC$自动机都不会单纯的建立字典树,而是建立。。$emm$。。大概是。。字典图的样子吧
我们重点分析一下结点$5$遍历时的情况:
显然,本来应该跳$2$次才能找到$7$号结点,但是我们通过$10$号结点的黑色边直接通过字母$s$找到了$7$号结点。
因此,就能在$O(1)$的时间内对单个结点构造$fail$指针。
这就是$build$完成的两件事:构建$fail$指针和建立字典图。这个字典图也会在查询的时候起到关键作用。
匹配:
总结:
到此,你已经理解了整个$AC$自动机的内容。我们一句话总结$AC$自动机的运行原理: 构建字典图实现自动跳转,构建失配指针实现多模式匹配。
手机扫一扫
移动阅读更方便
你可能感兴趣的文章