前置芝士:KMP算法、Trie树
在学习AC自动机和后缀自动机之前,我们需要简单了解一下自动机的定义:OI-Wiki 自动机
简单概括而言,自动机是对一个字符串进行目的性判断对数学模型,我们前面学过的KMP、Trie树包括今天要介绍的AC自动机等都是自动机模型。KMP算法是对一个单词在文章中的匹配,但如果有多个单词,例如这题:
如果直接遍历所有的模式串,对每个模式串进行KMP匹配,复杂度为,很明显是不行的。AC自动机的思路就是通过建立一棵Trie树,把所有的子串搬到Trie树上,然后一起匹配,那么时间复杂度就可以降到
。我们来看看是怎么建立的:
1、先对所有的模式串建立Trie树
然后对每一个节点构建next指针,next指针的作用和kmp里的一样,指向了与当前节点的后缀相同的最大前缀,例如最左下角的e节点,它的匹配的最大真后缀应该是h、e,所以应该指向右边的e节点
如果这个节点没有与之匹配的前缀,那么它的next就应该指向根节点。
我们把she这条链的next指针都画出来
根据she的next指针的构造我们可以发现一个规律:
设u是v的父节点,u的next指向p;
1、如果节点p有与v相同的子节点k,那么v的next指针就指向k。
不难证明,如果v的最大后缀不是k,那么把最后一个字母去掉的最大后缀也应该不止v,与u的next指针矛盾。
我们再吧shr的指针画出来
由于r的父节点h所指向的h没有r的子节点,所以r应该指向h的next指针
2、如果节点v的不满足第一条规律,那么v应该指向p的next指针,接着判断是否满足第一条规律
3、如果某次判断的指针为根结点,那么停止,当前节点v的next指针应该指向根节点
因此我们每一层的节点的next指针都可以由上一层的next指针推出来,那么求这棵Trie树的next指针只需要跑一遍BFS即可。
while (hh <= tt) { //每层遍历
int t = q[hh++]; //当前要判断的节点
for (int i = 0; i < 26; i++) { //所有的子节点
int p = tr[t][i]; //子节点p
if (!p) continue;
int j = ne[t]; //指向的节点j是否有相同子节点
while (j && !tr[j][i]) j = ne[j]; //依次向上找,到根结点为止
if (tr[j][i]) j = tr[j][i]; //如果根结点满足条件1,就向下走,否则j就是根结点
ne[p] = j;
q[++tt] = c; //加入队列
}
}
有同学可能会问了,如果匹配失败就沿着next指针的next依次向下找,那要是每次都失败了,那岂不是变成了遍历一整条链?
是这样没错,所以我们要对“依次向上找”这个过程进行优化,假设我们某次匹配的过程是这样
假设u、v、p的某个孩子节点的next指针都要指向根,但是我们每次匹配失败的时候都要沿着绿色线走一遍,这也太费时间了 ,那么就可以考虑路径压缩,将原本不存在的一些点设置为其next指针所在位置就可以了。(相当于我们把每个节点26个孩子全部用上,所有的孩子都指向应该指向的next指针,那么接下来再进行匹配就不会出现匹配失败的情况了)
构造代码修改如下:
while (hh <= tt) {
int t = q[hh ++ ];
for (int i = 0; i < 26; i ++ ) {
int p = tr[t][i];
int j = ne[t];
if (!p) tr[t][i] = tr[j][i]; //如果没有这个儿子,那就利用它
else {
ne[p] = tr[j][i]; //必定能匹配成功
q[ ++ tt] = p;
}
}
}
本题是统计出现的关键词数量,那么如果she存在,he就必然存在,我们在统计时沿着某一条next把沿途所有出现的关键词加上即可。
这样一来AC自动机完全体就跃然纸上了,Trie树的代码不再多言,下面放出完整代码供参考:
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 10010, S = 55, M = 1000010;
int n;
int tr[N * S][26], cnt[N * S], idx;
char str[M];
int q[N * S], ne[N * S];
void insert()
{
int p = 0;
for (int i = 0; str[i]; i ++ )
{
int t = str[i] - 'a';
if (!tr[p][t]) tr[p][t] = ++ idx;
p = tr[p][t];
}
cnt[p] ++ ;
}
void build()
{
int hh = 0, tt = -1;
for (int i = 0; i < 26; i ++ )
if (tr[0][i])
q[ ++ tt] = tr[0][i];
while (hh <= tt)
{
int t = q[hh ++ ];
for (int i = 0; i < 26; i ++ )
{
int p = tr[t][i];
if (!p) tr[t][i] = tr[ne[t]][i];
else
{
ne[p] = tr[ne[t]][i];
q[ ++ tt] = p;
}
}
}
}
int main()
{
int T;
scanf("%d", &T);
while (T -- )
{
memset(tr, 0, sizeof tr);
memset(cnt, 0, sizeof cnt);
memset(ne, 0, sizeof ne);
idx = 0;
scanf("%d", &n);
for (int i = 0; i < n; i ++ )
{
scanf("%s", str);
insert();
}
build();
scanf("%s", str);
int res = 0;
for (int i = 0, j = 0; str[i]; i ++ )
{
int t = str[i] - 'a';
j = tr[j][t];
int p = j;
while (p)
{
res += cnt[p];
cnt[p] = 0;
p = ne[p];
}
}
printf("%d\n", res);
}
return 0;
}
感谢观看,我们下期再见!