Kmp 是在一个文本串中,求一个模式串的出现次数。
AC 自动机是求有多少个模式串在一个文本串中出现过。
AC 自动机
=
=
=Trie
+
+
+ Kmp
fail 指针
暴力的写法是从 Trie 走,失配就回到根节点。
和 kmp 的 nxt 数组相似的,如果求出了每个点的 fail 指针,那么在失配后,可以直接跳到 fail 指针指向的点。那么如何求 fail 呢?
- 令根节点的深度为 1 1 1,那么对于所有深度为 2 2 2 的点,它们的 fail 指针都指向根节点。
- 从根节点进行 bfs,对于一个点 x x x,如果没有儿子,那么把它的儿子设为 f a i l ( x ) fail(x) fail(x)。
- 否则,将 f a i l ( y ) fail(y) fail(y) 设为 s o n f a i l ( x ) son_{fail(x)} sonfail(x)。
void getfail() {
queue <int> q;
for (int i = 0; i < 26; i++)
if (trie[0][i]) {
fail[trie[0][i]] = 0;
q.push(trie[0][i]);
}
while (!q.empty()) {
int x = q.front(); q.pop();
for (int i = 0; i < 26; i++){
int y = &trie[x][i];
if (y) {
fail[y] = trie[fail[x]][i];
q.push(y);
} else y = trie[fail[x]][i];
}
}
}
查询
cnt 为该模式串出现的次数。
int query(string s) {
int x = 0, res = 0;
for (int i = 0; i < s.size(); i++) {
x = trie[x][s[i] - 'a'];
for (int j = x; j && vis[j] != -1; j = fail[j]) {
res++;
vis[j] = -1; //因为求的是多少个不同的模式串,所以不能重复统计
}
}
return res;
}
加强
如果要求出现次数最多的模式串怎么办呢。
只要在建 Trie 树的时候,标记每个点属于哪个模式串,在查询时也把答案存到对应的模式串中即可。值得一提的是,因为要重复统计,所以在 query 中不要打标记。
二次加强
刚刚的 AC 自动机,fail 是在暴力跳的,最坏复杂度是 O ( t p ) O(t p) O(tp) 的,因为每次跳 fail 只让深度减少一,而且还没有打标记。那么现在要对 AC 自动机进行优化,让每个点只经过一次。
可以发现,对于文本串的每个字符,我们都要去暴力跳一下 fail,那么跳到的 fail 直接计数器加一。那么可以用懒标记的思想,在每个点打个标记,表示它要跳多少次 fail。最后再算总账,这样每个点就跳了一次。
不过要确定跳的顺序,一定先把深度大的标记传给深度小的,再把深度小的传给深度更小的。那么把 fail 指针看成有向边,这样可以拓扑排序来更新了。
void topsort() {
queue <int> q;
for (int i = 1; i <= cnt; /*Trie 节点数*/ i++)
if (!in[i]) q.push(i); // fail 指针入度为 0
while (!q.empty()) {
int x = q.front(); q.pop();
vis[trie[x].flg] = trie[x].tag; // 每个模式串出现的次数
int y = trie[x].fail; in[y]--;
trie[y].tag += trie[x].tag;
if (!in[y]) q.push(y);
}
}