AC 自动机是一种字符串匹配算法,而不是自动让你 AC 的作弊工具……(说实话我也不知道为什么它叫 AC 自动机)
它主要是解决这一类问题:给定 个模式串 和一个文本串 ,让 和这 个 做匹配。
例如这道模板题,就是要求文本串里有多少个模式串出现了。
考虑对于这种有多个模式串的匹配问题。我们可以先对于所有模式串建一棵 trie 树,然后在 trie 上跑匹配。
注意到在任意时刻,如果我们匹配到了节点 ,那么我们可以跳到所有表示的字符串是 所表示的字符串的后缀的节点 。
而为了不重不漏,我们肯定要跳到表示字符串长度最长的那一个点,因为从那个点继续跳就可以访问完所有表示的字符串是 所表示的字符串的节点了。
可以用一个数组来存这个东西,令 存表示的字符串是 的一个后缀且最长的节点 。
先考虑 数组的求解,由于 的深度肯定小于 的深度,所以我们可以使用 bfs 来求解。很明显,,对于所有节点 的儿子 ,。对于一个节点 的儿子 ,设 的边权为 ,那么 。
在求解 数组时,我们要顺便把 的所有边权为 且不存在的儿子 赋值为 。这是为了令匹配的时候如果没有儿子可以不用暴力跳 ,可以 重新开始匹配。
求 数组的代码如下:
inline void init() // 求解 fail 数组
{
queue<int> q; // bfs 用的队列
fail[1]=1; // fail[1] = 1
for(int i=0;i<26;i++)
{
int v=son[1][i];
if(v!=0) // 有这个儿子,fail 设置为 1 + 入队
{
fail[v]=1;
q.push(v);
}
else // 没有这个儿子,造一个!
{
son[1][i]=1;
}
}
while(!q.empty())
{
int u=q.front();
q.pop();
int fiu=fail[u]; // 父亲的 fail
for(int i=0;i<26;i++)
{
int v=son[u][i];
if(v!=0) // 有这个儿子,设置 fail + 入队
{
fail[v]=son[fiu][i];
q.push(v);
}
else // 没有这个儿子,造一个!
{
son[u][i]=son[fiu][i];
}
}
}
}
有了 数组,匹配也很简单了:
inline int slove() // 匹配
{
int u=1,res=0;
int len=strlen(str+1); // str 是一个全局变量,调用 slove() 时存的是文本串
for(int i=1;i<=len;i++)
{
int id=str[i]-'a';
u=son[u][id]; // 在 trie 树(trie 图?)上继续走一步
int tmp=u;
// 计算当前能匹配到的所有字符串对答案的贡献
while(tmp!=1&&sum[tmp]!=-1) // 跳到根节点或者跳到过就不用跳
{
res+=sum[tmp]; // 计算贡献
sum[tmp]=-1; // 标记跳到过了
tmp=fail[tmp]; // 继续跳 fail
}
}
return res;
}
这道题就做完啦,给出完整代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
int n;
char str[1000005];
int cnt,son[1000005][26];
int fail[1000005],sum[1000005];
inline void ins()
{
int u=1;
int len=strlen(str+1);
for(int i=1;i<=len;i++)
{
int id=str[i]-'a';
if(son[u][id]==0)
{
son[u][id]=++cnt;
}
u=son[u][id];
}
sum[u]++;
}
inline void init() // 求解 fail 数组
{
queue<int> q; // bfs 用的队列
fail[1]=1; // fail[1] = 1
for(int i=0;i<26;i++)
{
int v=son[1][i];
if(v!=0) // 有这个儿子,fail 设置为 1 + 入队
{
fail[v]=1;
q.push(v);
}
else // 没有这个儿子,造一个!
{
son[1][i]=1;
}
}
while(!q.empty())
{
int u=q.front();
q.pop();
int fiu=fail[u]; // 父亲的 fail
for(int i=0;i<26;i++)
{
int v=son[u][i];
if(v!=0) // 有这个儿子,设置 fail + 入队
{
fail[v]=son[fiu][i];
q.push(v);
}
else // 没有这个儿子,造一个!
{
son[u][i]=son[fiu][i];
}
}
}
}
inline int slove() // 匹配
{
int u=1,res=0;
int len=strlen(str+1); // str 是一个全局变量,调用 slove() 时存的是文本串
for(int i=1;i<=len;i++)
{
int id=str[i]-'a';
u=son[u][id]; // 在 trie 树(trie 图?)上继续走一步
int tmp=u;
// 计算当前能匹配到的所有字符串对答案的贡献
while(tmp!=1&&sum[tmp]!=-1) // 跳到根节点或者跳到过就不用跳
{
res+=sum[tmp]; // 计算贡献
sum[tmp]=-1; // 标记跳到过了
tmp=fail[tmp]; // 继续跳 fail
}
}
return res;
}
int main()
{
scanf("%d",&n);
cnt=1;
for(int i=1;i<=n;i++)
{
scanf("%s",str+1);
ins();
}
init();
scanf("%s",str+1);
printf("%d\n",slove());
return 0;
}
练习题目: