【字符串】AC自动机
使用场景
多模式串匹配问题
Fail 数组
\(fail\) 数组是 AC 自动机较为重要的一部分,
与 KMP 中 \(nxt\) 数组的地位相似,
但是 \(fail\) 数组具体是用来干什么的呢,
打个比方,你有两个字符串,分别为 SheAKioi 、 AKioi 、 heioi 、 noi,
特别的第一者称为文本串,后几者称为模式串,
你需要找到有几个模式串在文本串中出现。
如果用朴素算法的话,你需要一个一个枚举模式串,与文本串跑 KMP,
设文本串长度为 \(n\) ,第 \(i\) 个模式串长度为 \(m_i\) ,则时间复杂度为 \(O( {\textstyle \prod_{n+m_i}^{i\in n}} )\),
并不很优,甚至可以算作时间复杂度较为劣的算法,这时候我们就需要 AC 自动机来帮助我们解决此题。
回到前面,我们需要一种比普通方式更加优的算法解决多模式串问题,
注意到,当一个模式串为另一个已被包含的模式串的后缀时,则此模式串必定被文本串所包含,
每查找到一个字符串,就将他的所有后缀算上贡献,这样就不用考虑相交的情况,自然指针也不用回头,这大大优化了时间复杂度。
读者可能很快就能想到,这个 \(fail\) 数组就是用来存每个字符串串的后缀字符串,
正确,但是更准确来说,它类似一个指针,
现在你只需要看以下的图与代码就能搞懂了!

如图是一个 trie 树,红色节点代表这有一个字符串。
inline void get_fail() {
for (int i = 0; i < 26; ++i) {
if (trie[0][i]) {
q.push(cur[0][i]); // 先加入起始节点入队
}
}
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = 0; i < 26; ++i) {
if (trie[u][i]) { // 当有子节点
fail[trie[u][i]] = trie[fail[u]][i]; // 让该节点的 Fail 指针指向父节点的同字符儿子
//这里解释一下,因为子节点的后缀包含父节点的后缀,所以父节点的后缀加上子节点多出的一个字符,就是子节点的后缀辣
q.push(trie[u][i]);
} else {
trie[u][i] = trie[fail[u]][i]; // 让 fail 成为子节点,优化便利
}
}
}
}
inline void query(char c[]) {
int n = strlen(c + 1);
int now = 0;
for (int i = 1; i <= n; ++i) {
now = trie[now][c[i] - 'a'];
for (int t = now; t && bj[t] != -1; t = fail[t]) { // 便利后缀,并加上贡献
ans += bj[t];
bj[t] = -1;
}
}
}
也许有比我蒟的蒟蒻,所以这里再放一下建 trie 树的代码,以供参考:
inline void insert(char c[]) { // c[] 表示当前插入的字符串
int len = strlen(c + 1);
int now = 0; // now 表示此时的便利编号
for (int i = 1; i <= len; ++i) {
if (!cur[now][c[i] - 'a']) { // 如果不存在该节点
cur[now][c[i] - 'a'] = ++cnt; // 在 trie 树上建立一个新的节点
}
now = cur[now][c[i] - 'a']; // 下一个
}
bj[now] ++; // 标记来说明这里有一个字符串
}
实现步骤
- 由模式串 \(s_i\) 建立一颗字典树 \(trie[]\) ,\(num_i\) 表示节点 \(i\) 染色的次数;
- 枚举文本串 \(s\) 的下标 \(i\) ,在 trie 树上跑节点 \(j\) ;
- 维护 \(fail[]\) 数组,\(fail_x\) 表示节点 \(x\) 失配时下一步的位置;

浙公网安备 33010602011771号