先上比赛模板
#include<iostream>
#include<vector>
#include<algorithm>
#include<string.h>
#include<string>
#include<cstdio>
#include<queue>
using namespace std;
#define MAX 500010
#define ll int
//t:trie树 val[i]:标记i是否是一个字符串的结尾 fail[i]:i的失配指针 cnt:节点计数
ll t[MAX][26], val[MAX], fail[MAX], cnt;
// 传入字符串s,初始化字典树
void init(string s) {
ll len = s.size(); ll now = 0;
for (int i = 0; i < len; i++) {
int v = s[i] - 'a';
if (!t[now][v]) t[now][v] = ++cnt;
now = t[now][v];
}
val[now]++;
}
// 寻找所有失配指针
void build() {
queue<ll> q;
// 第一层节点的失配指针指向根0,全部入队t[0][i]是对应的节点编号
for (int i = 0; i < 26; i++)if (t[0][i])fail[t[0][i]] = 0, q.push(t[0][i]);
while (!q.empty()) {
int u = q.front(); q.pop();
for (int i = 0; i < 26; i++) {
//u的子节点i的失配指针,等于u的失配指针下同为i的节点编号,如果不存在就是根
if (t[u][i])fail[t[u][i]] = t[fail[u]][i], q.push(t[u][i]);
//不存在这个子节点 那么当前节点的这个子节点指向当前节点fail指针的这个子节点
else t[u][i] = t[fail[u]][i];
}
}
}
int query(string s) {
int len = s.size(); int now = 0, ans = 0;
for (int i = 0; i < len; i++) {
now = t[now][s[i] - 'a'];//下一层
for (int t = now; t && val[t] != -1; t = fail[t])//当此节点存在同时其未被遍历;
ans += val[t], val[t] = -1;//当这个值为负时代表已经统计过了
}
return ans;
}
int main() {
int n; cin >> n; string s;
for (int i = 0; i < n; i++) {
cin >> s;
init(s);
}
build();
cin >> s;
cout << query(s) << endl;
}
关于AC自动机的教程已经很多了,最难理解的部分无非是失配指针的构造和跳转移边的原理。所谓失配指针无非是KMP在字典树上的形式,多画画图也还好。让我纠结的反而是跳转移边的必要性和原理。
对上图,我们在构建fail数组时,有一条语句else t[u][i] = t[fail[u]][i];
,这也就是所谓的跳转移边优化。当我们build到最左边的节点d的时候,他的fail节点是他的父节点的fail节点的d节点的编号,而他的下属所有不存在的节点都会执行跳转移边,具体而言就是左下角的e节点本身是不存在的,此时我们执行t[u][i] = t[fail[u]][i]
也就是右边他的fail节点的e节点的编号给他。当我们查询的时候,我们需要匹配的串是abcde,那么匹配到左下角的d节点我们会统计一次(因为d是abcd的结尾字符),然后now=t[now]['e'-'a']
,(now是d的编号),now经过修改指向了右侧的e节点(也就是说我们在这里建立了一个虚拟的e节点,指向其可以得到匹配的地方),然后我们直接将e节点统计一次(e是bcde的终止节点),就得到了答案。如果没有这一步跳转移边,我们就需要每次都跳fail,如果上图的e节点在根节点上,那么跳转移边的结果是所有虚拟的e节点都指向0,只需要一步跳转。而跳fail节点的话,需要4步(每个d向fail节点跳,跳完还不是不能匹配e,继续跳,直到根节点)