Trie + KMP的结合体 AC自动机 -> Trie图
将next数组搬到trie里,每个节点的next值指以当前节点为终点的和以根节点为起点的非平凡(不能从根节点到当前节点的子串)的最长公共前后缀的长度。
KMP本质就是AC自动机在一条链上的情况。
0x00 求next
bfs求解next,将KMP求next的过程类比到trie上
// 优化前// 求nextstatic void build() {int hh = 0, tt = -1;for (int i = 0; i < 26; i++) {if (tr[0][i] != 0)q[++tt] = tr[0][i];}while (hh <= tt) {int u = q[hh++];for (int i = 0; i < 26; i++) {int c = tr[u][i];if (c == 0) continue;int j = ne[u];while (j > 0 && tr[j][i] == 0)j = ne[j];if (tr[j][i] != 0)j = tr[j][i];ne[c] = j;q[++tt] = c;}}}
Trie图
// 优化后static void build() {int hh = 0, tt = -1;for (int i = 0; i < 26; i++) {if (tr[0][i] != 0)q[++tt] = tr[0][i];}while (hh <= tt) {int u = q[hh++];for (int i = 0; i < 26; i++) {int c = tr[u][i];if (c == 0) {tr[u][i] = tr[ne[u]][i]; // 一步到位} else {ne[c] = tr[ne[u]][i];q[++tt] = c;}}}}
优化后,tr[u][i]直接指向首个匹配,若没有匹配,指向0。省掉了一维while
0x01 匹配
// 优化前的匹配String s = sc.next();int res = 0;for (int i = 0, j = 0; i < s.length(); i++) {int id = s.charAt(i) - 'a';while (j > 0 && tr[j][id] == 0)j = ne[j];if (tr[j][id] != 0)j = tr[j][id];int p = j;while (p > 0) {res += cnt[p];cnt[p] = 0;p = ne[p];}}
Trie图
// 优化后的匹配String s = sc.next();int res = 0;for (int i = 0, j = 0; i < s.length(); i++) {int id = s.charAt(i) - 'a';j = tr[j][id];int p = j;while (p > 0) {res += cnt[p];cnt[p] = 0;p = ne[p];}}
0x03 应用
AcWing 1053. 修复DNA
AC自动机 + 状态机DP
思路:
状态表示:f[i][j]表示考虑文本串的前i个字符,当前走到了AC自动机的位置j的所有操作方案中最少的字符修改数量。
AcWing 1285. 单词
AC自动机 + 拓扑DP
目的:求每个字符串出现的次数
一个字符串出现的次数= 所有满足要求的前缀个数(要求是前缀的后缀包含该字符串)
考虑每个前缀字符串的后缀可以等于哪些目标字符串。正好符合next数组的定义,通过递归就能统计到所有的目标串。
递归的方法就是按照拓扑序的倒序,因为Trie图本身是有向无环图
