前置知识
简介
AC自动机是在以字典树为基础的数据结构下,结合
K
M
P
\;KMP\;
KMP算法的思想所建立的一种更为高级的数据结构说不说这句话其实没区别,常用于多模式串的匹配,通过引入一个
f
a
i
l
\;fail\;
fail指针来减少匹配的回溯次数,更高效的进行匹配。
实现
首先最根本的就是先要由多个模式串建立起字典树,然后开始构造
f
a
i
l
\;fail\;
fail指针,观察如下图所示:
画图实在太费事了,引用图片来源于OI Wiki AC自动机,由衷表示感谢
观察这个过程,首先不难得出首先构建
f
a
i
l
\;fail\;
fail指针的过程与
b
f
s
\;bfs\;
bfs相同,即每次取出子节点来设置其
f
a
i
l
\;fail\;
fail指针,这里
f
a
i
l
\;fail\;
fail指针定义为指向所有模式串的前缀中匹配当前状态的最长后缀。文字太抽象了,看上图可能好理解一点
那么如何来构建某个结点的
f
a
i
l
\;fail\;
fail指针的呢。如下图所示,考虑
f
a
i
l
[
6
]
\;fail[6]\;
fail[6]如何计算
如果
t
r
i
e
[
p
o
s
]
[
s
[
i
]
]
\;trie[pos][s[i]]\;
trie[pos][s[i]]存在,那么就考虑其父结点的
f
a
i
l
\;fail\;
fail指向。如上图所示,
f
a
i
l
[
5
]
\;fail[5]\;
fail[5]指向了
10
\;10\;
10,而
10
10
10后没有
s
s
s,于是继续向上得到
f
a
i
l
[
10
]
=
0
\;fail[10]=0\;
fail[10]=0,此时
0
\;0\;
0后有字母
s
\;s\;
s,于是
f
a
i
l
[
6
]
\;fail[6]\;
fail[6]就指向
7
\;7\;
7,如此便完成了 构造指针的过程。代码实现如下:
inline void get_fail()
{
for (int i = 0; i < 26; i++)
{
if (trie[0][i])
{
q.push(trie[0][i]);
}
}
while (!q.empty())
{
int x = q.front();
q.pop();
for (int i = 0; i < 26; i++)
{
if (trie[x][i])
{
fail[trie[x][i]] = trie[fail[x]][i];
q.push(trie[x][i]);
}
else
{
trie[x][i] = trie[fail[x]][i];
}
}
}
}
尽管如何构造的思路已经有了,但是在实现代码上还是有许多需要剖析的地方。首先第一个循环就是将所有首字母入队,然后接下来的构建 f a i l \;fail\; fail指针的过程是极其重要的:首先从队列中弹出一个结点,然后遍历它的所有26个子节点,如果当前结点存在,那么就可以得到 f a i l [ t r i e [ x ] [ i ] ] = t r i e [ f a i l [ x ] ] [ i ] \;fail[trie[x][i]] = trie[fail[x]][i]\; fail[trie[x][i]]=trie[fail[x]][i],发现似乎与上文中分析的逐层找相同有所不同,为什么这里是直接赋值而不是循环来实现 f a i l \;fail\; fail指针呢?关键在于当某个结点的子节点不存在时,执行的 e l s e \;else\; else语句块中,将这些空节点与父结点的 f a i l \;fail\; fail指针关联起来,使得这些空结点也产生了隐含的 f a i l \;fail\; fail指针,这样就不用直接循环从而通过赋值在 O ( 1 ) \;O(1)\; O(1)内得到 f a i l \;fail\; fail指针的指向。
诚然这么说还是极其晦涩难懂的因为压根很难说清楚 ,还是用上面的例子来说明:
在计算
f
a
i
l
[
6
]
\;fail[6]\;
fail[6]时,通过父结点的
f
a
i
l
\;fail\;
fail指针找到了
t
r
i
e
[
10
]
[
′
s
′
−
′
a
′
]
\;trie[10]['s'-'a']
trie[10][′s′−′a′],本来这个位置应该是空的,但是在先前弹出结点
10
\;10\;
10时,它的每个结点都和它的父结点的
f
a
i
l
\;fail\;
fail指针关联,在先前就已经将
t
r
i
e
[
10
]
[
′
s
′
−
′
a
′
]
\;trie[10]['s'-'a']\;
trie[10][′s′−′a′]设置为
t
r
i
e
[
f
a
i
l
[
10
]
]
[
′
s
′
−
′
a
′
]
\;trie[fail[10]]['s'-'a']\;
trie[fail[10]][′s′−′a′],即
t
r
i
e
[
0
]
[
′
s
′
−
′
a
′
]
\;trie[0]['s'-'a']\;
trie[0][′s′−′a′],而这一位置是有指向的,将其指向
7
\;7\;
7号结点,正是因为之前将所有空结点的位置也使得其产生指向,所以再后面构建时才能更快地得到
f
a
i
l
\;fail\;
fail指针的指向。
(
p
s
\;ps\;
ps:将整个二维数组全部画出来其实可以更直观的看出这个指向的变化,但是制图是在太耗时了,难以理解的可以手动模拟一下,就可以发现其实这是将一个字典树改变为字典图的过程。)
有了 A C AC AC自动机后接下来就要考虑如何进行匹配了,先给出实现代码:
int query(char* str)
{
int pos = 0, res = 0;
for (int i = 0; str[i]; i++)
{
pos = trie[pos][str[i] - 'a'];
for (int j = pos; j && ~e[j]; j = fail[j])
{
res += e[j];
e[j] = -1;
}
}
return res;
}
这里开始循环每个字符,先通过字典树得到位置,然后逐一进行匹配,如果匹配成功,那么就一直跳转 f a i l \;fail\; fail指针,同时将标记为改为-1,避免之后重复匹配陷入死循环,最后的 r e s \;res\; res就是匹配到模式串的数量。