AC 自动机的一些匹配板子
wing_heart · · 算法·理论
AC 自动机的一些匹配板子
原文链接
AC 自动机介绍
参考 OI-Wiki AC 自动机。
如果你已经会 AC 自动机(包括 fail 树的构建),可以跳过这部分。
自动机
参考 OI-Wiki 有限状态自动机。
支持的功能
AC 自动机可以对多个
但是建好之后似乎不支持再插入新串。
建树流程概括
-
建出 trie 树。
把所有
s 插入 trie。字符串从前往后插入。 -
构建适配指针
fail 。
struct node {
int fail;
// int tg;
int son[26];
}tr[N];
int cnt;
void insert(char *s,int n) { // 在字典树中插入串串 s
int u = 0;
rep(i,0,n-1) {
int c = s[i]-'a';
if(!tr[u].son[c]) tr[u].son[c] = ++cnt;
u = tr[u].son[c];
}
// 更新 tr[u]
}
void build() { // 建 fail 树
// 若 rt 非 0,则需要把 tr[rt] 的所有空儿子,和 tr[rt].fail 设成 rt
queue<int> que;
rep(i,0,25) if(tr[0].son[i]) que.push(tr[0].son[i]); // tr[tr[0].son[i]].fail = rt
while(que.size()) {
int u = que.front();
que.pop();
rep(i,0,25) {
if(tr[u].son[i]) {
tr[tr[u].son[i]].fail = tr[tr[u].fail].son[i];
que.push(tr[u].son[i]);
} else tr[u].son[i] = tr[tr[u].fail].son[i];
}
}
}
怎么构建失配指针?
按照 bfs 的顺序求
现在
- 若
fail_{fa_{u}} 经过c 可以到达状态v ,则fail_u = v 。 - 否则,递归查
fail_{fail_{fa_u}} 。 - 以此类推。
特判
欸,它的时间复杂度是多少?
跳
具体怎么证明?
可以用势能分析一下吧,势能定义为在队列里的
跳
所以总共消耗掉的势能是
因为每个点只有一个
代码见上面。
多模式匹配
对于字典
顺序枚举
- 遍历
u 在fail 树上到根的fail 链,找到所有后缀状态,加进答案。 - 如果原 trie 上
u 经过c 可以到达v ,就直接走了。 - 否则看看
fail_u 能否经过c 到达v 。 - 否则跳下一个适配指针。
复杂度最坏是
需要优化。
求哪些 s_i 在 t 中出现了
这个不用优化,直接暴力跳 fail 指针,不要跳已经跳过的节点即可。
P3808 AC 自动机(简单版),code。
求每个 s_i 在 t 中出现多少次
在 AC 自动机里面跑
- 最后对
fail 树拓扑排序(内向树)。然后按照拓扑序记录答案。 - 也可以 dfs,按照 dfs 序逆序更新答案,或者递归回溯的时候更新答案。
只是不同的写法而已。
P5357 【模板】AC 自动机,code。
对每个 t_i ,求所有 s 在 t_i 中出现次数之和
每个状态
然后跑
类似题目:P14363 [CSP-S 2025] 谐音替换 / replace,code。
对每个 t_i ,求有多少 s 是它的字串
类似题目:P2336 [SCOI2012] 喵星球上的点名
https://www.luogu.com.cn/discuss/1190105
- 简单的哈希做法,复杂度根号。按照字符串长度分类,一共只有根号种不同的长度。然后分类哈希。https://www.luogu.com.cn/article/zevrys8n
- 单老哥做法。跑
t 的时候每个状态u 需要记录u 在 fail 树上到根的链中,没有被算过的点。可以使用重链剖分维护每个重链的哪个前缀以及使用过;或者建所有u 的虚树,然后在虚树上面统计,怎么统计都行吧。
对每个 s_i ,求它在几个 t_i 中出现了
类似题目:P5840 [COCI 2014/2015 #5] Divljak
跑
上面的例题,每个
对每个 s_i ,求它在所有 t_i 出现的次数之和
跑