Trie + KMP的结合体 AC自动机 -> Trie图
将next数组搬到trie里,每个节点的next值指以当前节点为终点的和以根节点为起点的非平凡(不能从根节点到当前节点的子串)的最长公共前后缀的长度。
KMP本质就是AC自动机在一条链上的情况。
0x00 求next
bfs求解next,将KMP求next的过程类比到trie上
// 优化前
// 求next
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) 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图本身是有向无环图