【字符串】AC自动机

使用场景

多模式串匹配问题

Fail 数组

\(fail\) 数组是 AC 自动机较为重要的一部分,

与 KMP 中 \(nxt\) 数组的地位相似,

但是 \(fail\) 数组具体是用来干什么的呢,

打个比方,你有两个字符串,分别为 SheAKioiAKioiheioinoi

特别的第一者称为文本串,后几者称为模式串,

你需要找到有几个模式串在文本串中出现。

如果用朴素算法的话,你需要一个一个枚举模式串,与文本串跑 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] ++;  // 标记来说明这里有一个字符串
}

实现步骤

  1. 由模式串 \(s_i\) 建立一颗字典树 \(trie[]\)\(num_i\) 表示节点 \(i\) 染色的次数;
  2. 枚举文本串 \(s\) 的下标 \(i\) ,在 trie 树上跑节点 \(j\)
  3. 维护 \(fail[]\) 数组,\(fail_x\) 表示节点 \(x\) 失配时下一步的位置;

课后练习

Luogu P3808

Luogu P3796

Luogu P5357

posted @ 2023-01-11 18:49  SenGYi  阅读(60)  评论(0)    收藏  举报