突然想起来还有这么一个东西我没有写过学习笔记的
AC 自动机的三个板子
首先你需要会字典树,最好会 kmp ,不会也无所谓。
AC 自动机我个人觉得和 \(kmp\) 没啥联系,只和字典树有关系2333,因为 AC 自动机是基于字典树上的bfs
建造出来的。
如果给你几个串,你 \(kmp\) 的复杂度显然不可以接受,所以就需要我们的 AC 自动机了。
我个人习惯从 \(cnt = 1\) 开始,即字典树的 \(root\) 为 \(1\),以及 \(ch_{u,i}\) 来表示字典树的节点,我更倾向于把字典树的一个节点当做是字符串,因为从根节点顺次下来确实是一个字符串。
首先你要明白 \(fail_i\) 和 \(i\) 的关系是 \(fail_i\) 是 \(i\) 的子串,以及,当你构造完 AC 自动机之后,\(ch_{u,i}\) 不一定是原字典树的样子,就是说会相对于字典树的结构变化,比如说跨越层,但是并不难理解,其实就是为了 \(fail\) 的转移。
如果实际需要,比如说 这一题,你就可以先复制原有节点,然后建 AC 自动机。
\(fail_i -> i\) 连边会成为一棵树,而树内查询之类的可以用树状数组等数据结构维护,其中有一条重要性质:\(i\) 的子树内所包含的节点,都是包含 \(i\) 这个子串的(我上面说过我喜欢把节点作为字符串了)
总感觉不是很好讲,那我来画图吧。 举例子是最好的办法。 为了方便画图,我给出两个字符串 "ab" "bab"
图1: 建成一颗字典树
图2:建一个 AC 自动机
如何构建呢?我把构建好的放出来。在这个图里 \(i\) 指向 \(fail_i\)
举个栗子,我们先找到 "bab" 对应的节点 \(i\) ,然后你发现 \(fail_i\) 指向的节点所对应的字符串是 "ab",以及找到其他的 \(fail_i\) 和 \(i\) 的关系都是这样。
即 \(fail_i\) 所对应的字符串是 \(i\) 所对应字符串的子串并且是 \(i\) 的后缀。
如果我是 "abca" "babd" 呢,原有的 AC 自动机不用做任何变化,接着画就好了。
诶,然后你发现你的 "abc" 没办法找到一个 \(fail_i\),没关系,那就连到根节点,然后你会发现你的 "babd"也找不到一个相同后缀的子串,没关系,那也丢给根节点。
然后画成这个 b 样子了
然后你发现你的 "abca" 只能找到一个短短的相同后缀 "a",那就连上,所以 AC 自动机就建好了。
我们尝试画一个 AC 自动机的 \(fail\) 树。(其实就是把 \(fail_i\) 和 \(i\) 连起来嘛…)
我们就很自然的发现了 AC 自动机 \(fail\) 树的特点了(虽然我前面提到过)
代码
std ::queue<int> q; for (int i = 0; i < 26; i++) if (ch[1][i]) fail[ch[1][i]] = 1, q.push(ch[1][i]); else ch[1][i] = 1; while (!q.empty()) { int u = q.front(); q.pop(); for (int i = 0; i < 26; i++) if (ch[u][i]) fail[ch[u][i]] = ch[fail[u]][i], q.push(ch[u][i]); else ch[u][i] = ch[fail[u]][i]; } for (int i = 2; i <= cnt; i++) g[fail[i]].pb(i);
然后我们来看例题 P3808 【模板】AC自动机(简单版)
给定n个模式串和1个文本串,求有多少个模式串在文本串里出现过。
我们直接对 \(n\) 个模式串建 AC 自动机,然后在上面跑就好了,一直跳 \(fail\),查询以当前结尾的串有多少个子串的 \(end\) 节点是这个,用 \(ed_i\) 表示这个出现次数,不能反复计算,所以计算过的就弄成 \(-1\)。
也不会忽略上面的贡献,因为如果 \(ed_i == -1\),那么 \(fail_i,fail_{fail_i}\) 也一定是 \(-1\)。
int find(string s) { int ans = 0 , p = 0 ; for(char c : s) { int x = c - 'a' ; p = ac[p].son[x] ; for(int j = p ; j && ac[j].end != -1 ; j = ac[j].fail) ans += ac[j].end , ac[j].end = -1 ; } return ans ; }
咕咕咕
来源:https://www.cnblogs.com/Isaunoya/p/12457888.html