AC自动机的前置学习需求是Trie树和KMP算法,AC自动机相当于二维的KMP算法。
应用场景:求一堆字符串在一个文本中出现的次数
AC自动机
#include <bits/stdc++.h>
using namespace std;
const int N = 10010, M = 1000010;
int n;
int tr[N * S][26]; // Trie树的数组
int cnt[N * S]; //单词出现的次数
int ne[N * S]; //回溯的位置
int idx;
char str[M];
void insert()
{
int p = 0;
for (int i = 0; str[i]; i++)
{
int t = str[i] - 'a';
if (!tr[p][t])
tr[p][t] = ++idx;
p = tr[p][t];
}
cnt[p]++;
}
void build()
{
queue<int> q;
for (int i = 0; i < 26; i++)
if (tr[0][i])
q.push(tr[0][i]);
while (!q.empty())
{
int t = q.front();
q.pop();
for (int i = 0; i < 26; i++)
{
int p = tr[t][i];
int j = ne[t];
if (!tr[t][i])
continue;
while (j && !tr[j][i])
j = ne[j];
if (tr[j][i])
j = tr[j][i];
ne[p] = j;
q.push(p);
}
}
}
int query()
{
int res = 0;
for (int i = 0, j = 0; str[i]; i++)
{
int t = str[i] - 'a';
while (j && !tr[j][t])
j = ne[j];
if (tr[j][t])
j = tr[j][t];
/*
还需要加上该串的最大后缀(相当于是前缀, 这个后缀是由更小的后缀转移过来的)
如果该前缀是单词, 那么ne[p]是尾节点编号, cnt[ne[p]] > 0, 加上就行, 不是单词+0, 就相当于没加.
*/
int p = j;
while (p && cnt[p] != -1)
{
res += cnt[p];
cnt[p] = -1;
p = ne[p];
}
}
return res;
}
int main()
{
memset(tr, 0, sizeof tr);
memset(cnt, 0, sizeof cnt);
memset(ne, 0, sizeof ne);
idx = 0;
int n;
cin >> n;
for (int i = 0; i < n; i++)
{
scanf("%s", str);
insert();
}
build();
scanf("%s", str);
printf("%d\n", query());
return 0;
}
Trie图
原来需要一下一下跳直到找到可匹配前缀, 现在通过累积维护, 一层一层更新, 可以一步直接找到可匹配前缀的下标.
#include <bits/stdc++.h>
using namespace std;
const int N = 10010, S = 55, M = 1000010;
int n;
int tr[N * S][26]; // Trie树的数组
int cnt[N * S]; //单词出现的次数
int ne[N * S]; //回溯的位置
int idx;
char str[M];
void insert()
{
int p = 0;
for (int i = 0; str[i]; i++)
{
int t = str[i] - 'a';
if (!tr[p][t])
tr[p][t] = ++idx;
p = tr[p][t];
}
cnt[p]++;
}
void build()
{
queue<int> q;
for (int i = 0; i < 26; i++)
if (tr[0][i])
q.push(tr[0][i]);//将第二层所有出现了的字母扔进队列
while (!q.empty())
{
int t = q.front();
q.pop();
for (int i = 0; i < 26; i++)
{
int p = tr[t][i]; //一层一层的更新过来维护每个节点的最近的可匹配前缀编号, 由一个一个的跳, 变成了一步到位,t的儿子都没有i,那匹配的儿子里面的i必然是匹配不到了.空的还是要赋起走tr[t][i] = tr[ne[t]][i], 以便后面的不空的用tr[ne[t]][i]赋值
if (!p)
tr[t][i] = tr[ne[t]][i]; // 形成了一个链式关系, 当前节点的最大前缀一定是由前面的转移过来的
else
ne[p] = tr[ne[t]][i],
q.push(
p); // 可能j(ne[t])指针的儿子节点中没有i,但是tr[ne[t]][i]的值为离他最近的最大前缀坐标.(过滤掉了ne的子节点也没有i这个儿子节点的情况)
}
}
}
int query()
{
int res = 0;
for (int i = 0, j = 0; str[i]; i++)
{
int t = str[i] - 'a';
j = tr[j][t]; // 将原来需要跳多次, 通过维护最大前缀坐标, 就可以一次性跳转.
int p = j;
while (p && cnt[p] != -1)
{
res += cnt[p];
cnt[p] = -1;
p = ne[p];
}
}
return res;
}
int main()
{
memset(tr, 0, sizeof tr);
memset(cnt, 0, sizeof cnt);
memset(ne, 0, sizeof ne);
idx = 0;
scanf("%d", &n);
for (int i = 0; i < n; i++)
{
scanf("%s", str);
insert();
}
build();
scanf("%s", str);
printf("%d\n", query());
return 0;
}