AC 自动机学习笔记

1.引入

AC 自动机是一种结合 KMP 和字典树的有限状态自动机。
可以解决单主串多模式串匹配

2.思想

AC 自动机基于字典树的结构和 KMP 的思想组成。

2.1. 失配指针

对于字典树上每个点,可以看作是一个字符串的前缀,本文中指的前缀就是指字典树上的每个点,现在用 \(trie_{u, c}\) 表示。

失配指针 \(fail_u\) 就是指和点 \(u\)最长公共后缀且是真后缀的点 \(v\) 所代表的前缀。

当当前在点 \(trie_{u, c}\) 时,失配指针的具体求法:

  1. 如果存在 \(trie[fail[u]][c]\),那么 \(fail[u] = trie[fail[f]][c]\)
  2. 否则,就同 KMP 一样寻找 \(trie[fail[fail[u]]][c], trie[fail[fail[fail[u]]]][c], \cdots\),直到满足条件 \(1\)

至于为什么是最长的,这个很好理解。

举个例子,就如下图(稍微有点抽象),插入了 orzohmobaibaixie 图中红色的边即为失配指针。

pkbbdBD.png

3.实现

3.1. 求失配指针

我们考虑到直接这样做,对于每个模式串的复杂度为 \(\mathcal{O}(|S_i|)\)
总的复杂度就是 \(\mathcal{O}(\sum | S_i |)\)
在给定 trie 树的情况下,容易被卡
现在我们引入自动机的思想,给 trie 树增加转移边 \(next_c\)
对于每个节点维护 \(next_c\) 表示这个节点后面加上字符 \(c\) 后,fail 会跳到的位置
维护比较简单,现在的复杂度就是 \(\mathcal{O}(n |\Sigma|)\)

void GetFail(){
	std::queue<int> q;
	for (int i = 0; i < 26; i++) {
		if (trie[i][0]) {
			q.push(trie[i][0]);
		}
	}
	while (!q.empty()) {
		int u = q.front(); q.pop();
		for (int i = 0; i < 26; i++) {
			if (trie[i][u]) {
				fail[trie[i][u]] = trie[i][fail[u]];
				q.push(trie[i][u]);
			} else {
				trie[i][u] = trie[i][fail[u]];
			}
		}
	}
}

4.求解模式串出现次数问题

4.1 思路

对所有 \(S_i\) 建 AC 自动机,然后对 \(T\) 做匹配。一个 \(S_i\) 出现的次数等于匹配时有多少步停在了它 fail 树子树内。

根据这个思路不难设计 \(\mathcal{O}(| T | \log N)\) 的在线查询算法,和 \(\mathcal{O}(| T | + N)\) 的离线算法,\(N\) 是 AC 自动机状态数

4.2 例题

NOI2011 阿狸的打字机

给定一棵字典树,\(q\) 次询问,每次询问字典树上一个点到根路径构成的字符串,在另一个字典树上字符串的出现次数

考虑一般求模式串出现的方法,我们需要在 AC 自动机上暴力走转移边,然后对于每个点在 fail 树上将它到根的权值加一
这个题目给定的主串肯定在字典树上,只需要求解 fail 树上,这些节点在 \(y\) 子树中出现的次数
我们考虑离线下来,然后通过 dfs 序求解 fail 树上子树和就可以了

5.求解是否包含模式串问题

5.1 思路

考虑 AC 自动机的转移边构成一个有向图
我们限制不能包含模式串等价于在这个节点处的一段后缀没有模式串
也就是模式串对应的 end 节点在 fail 树不是当前节点的祖先
此时就转换为有向图长度为 \(k\) 的路径计数问题

5.2 例题

5.2.1 JSOI2007 文本生成器

5.2.1.1 题目描述

给定 \(n(1 \le n \le 60)\) 个字符串(\(s_1, s_2, \cdots, s_n\)),\(| s_i | \le 100\),问有多少个长度为 \(m(1 \le 100)\) 的字符串中至少包含一个字符串。

字符集 \(|\Sigma| = 26\)

5.2.1.2 过程

直接统计至少包含一个字符串的太麻烦,总方案数又很好求,因此考虑求出没有包含任何串的方案数。

因为考虑模式串的匹配,所以考虑使用 AC 自动机优化,状态改为 \(f_{i, j}\) 表示前 \(i\) 个字符跳到 AC 自动机上的第 \(j\) 个节点的方案数,转移也是枚举下一个位置填谁,然后考虑是否继承当前方案。

最后答案 \(ans = 26 ^ m - \sum f_{m, i}\),时间复杂度 \(\mathcal{O}(m \sum | s_i |\Sigma)\)

posted @ 2024-07-26 16:13  Z_drj  阅读(31)  评论(0)    收藏  举报