首先确定一件事,ac自动机其实这是kmp算法在trie树上面的一个扩展。就是相当于是在trie树上面kmp算法而已。kmp算法是一种字符串匹配算法(单字符串匹配),而ac自动机就是一种多模式匹配算法(用来一次性匹配多个字符串的)。
ac自动机里面的next数组存的是以当前字母结尾的字符串与从开头开始的字符串的最大匹配长度的字符串的指针。用下面题目的样例做演示如图:
如上图所示,she里面的h能够找到一个长度为1的前缀相等,next就直接指向另一个h的位置了,e也一样,能够找到一个长度为2的he前缀,所以直接指向前缀的最后一个字母e了。
构建next数组过程,首先看回kmp算法里面的next数组的构建过程,在下图里面的j代表的具体意义实际上是next[i-1],也就是第i-1个字符与前缀的最大匹配长度,下面的while循环里面a[i]和a[j+1]的比较实际上就是第i个字符和第i-1个字符能够匹配上的最大长度的前缀的下一个进行比较,如果相同就能让第i个字符的next[i]==next[i-1]+1;
void get_next()
{
int n=strlen(a+1);//因为要用到a[i-1],所以从1开始存
for(int i=2,j=0;i<=n;i++)
{
while(j&&a[i]!=a[j+1]) j=ne[j];
if(a[i]==a[j+1])j++;
ne[i]=j;
}
}
由kmp的匹配过程可以得知第i层的字符计算next值的时候需要用到第i-1层的next数值。
转换到AC自动机里面next数组的构建第i层的字符的next时同样也需要用到第i-1层的next信息。
这里需要将原本的for循环扩展成宽搜的形式, 用一个队列来实现逐层遍历
传送门:搜索关键词
思路:
求next数组的过程和匹配的过程大同小异,除了匹配的时候还要把所有具有相同前缀的完整单词都计入答案。反映到下图里面就是,当找到she这个完整单词的时候需要把he也记录进答案并将he的cnt置为0。
代码:
#include<iostream>
#include<algorithm>
#include<cstring>
#include<vector>
using namespace std;
typedef long long ll;
const int N=1e4+10,M=1e6+10,S=55;
int n;
int tr[N*S][26],cnt[N*S],idx;
char str[M];
int q[N*S],ne[N*S];
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()
{
int hh=0,tt=-1;
for(int i=0;i<26;i++)
{
if(tr[0][i])
q[++tt]=tr[0][i];
}
while(hh<=tt)
{
int t=q[hh++]; //t对应kmp里面的i-1
for(int i=0;i<26;i++) //遍历查看t的后继字母有哪些
{
int c=tr[t][i]; //c实际上对应kmp里面的第i个字母,就是
if(!c)continue; //如果不存在话就直接跳过
int j=ne[t]; //相当于原本kmp里面的next[i-1];
while(j&&!tr[j][i]) j=ne[j]; //如果tr[j][i]存在的话相当于kmp算法里面的next[i-1]的下一位和kmp算法里面的第i位是相等的
if(tr[j][i]) j=tr[j][i]; //能够匹配成功就可以直接把c的next指针指过去
ne[c]=j;
q[++tt]=c; //同时要把新扩展的一层钱全部入队用来计算下一层
}
}
}
int main()
{
int T;
scanf("%d",&T);
while(T--)
{
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);
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]; //能够匹配成功的话就要进行扩展
int p=j;
while(p) //
{
res+=cnt[p]; //匹配到一个完整的单词的时候,这里不会是0
cnt[p]=0; //
p=ne[p]; //顺便把沿途上面所有的完整单词都记录一次。
}
}
printf("%d\n",res);
}
return 0;
}
Trie图优化版本(写ac自动机通常都是用的这个版本):
这个版本直接将tr数组改成指向能够匹配的上的节点的编号。
#include<iostream>
#include<algorithm>
#include<cstring>
#include<vector>
using namespace std;
typedef long long ll;
const int N=1e4+10,M=1e6+10,S=55;
int n;
int tr[N*S][26],cnt[N*S],idx;
char str[M];
int q[N*S],ne[N*S];
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()
{
int hh=0,tt=-1;
for(int i=0;i<26;i++)
{
if(tr[0][i])
q[++tt]=tr[0][i];
}
while(hh<=tt)
{
int t=q[hh++]; //t对应kmp里面的i-1
for(int i=0;i<26;i++) //遍历查看t的后继字母有哪些
{
int p=tr[t][i];
if(!p) tr[t][i]=tr[ne[t]][i]; //如果没有这个后继节点,则将其指向有可能有这个后继节点的
else
{
ne[p]=tr[ne[t]][i];
q[++tt]=p;
}
}
}
}
int main()
{
int T;
scanf("%d",&T);
while(T--)
{
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);
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)
{
res+=cnt[p];
cnt[p]=0;
p=ne[p];
}
}
printf("%d\n",res);
}
return 0;
}
传送门:单词
思路:没想好,不写先
代码:
#include<iostream>
#include<algorithm>
#include<cstring>
#include<vector>
using namespace std;
typedef long long ll;
const int N=1e6+11;
int n;
int tr[N][26],f[N],idx;
int q[N],ne[N];
char str[N];
int id[210];
void insert(int x)
{
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];
f[p]++;
}
id[x]=p;
}
void build()
{
int hh=0,tt=-1;
for(int i=0;i<26;i++)
if(tr[0][i])
q[++tt]=tr[0][i];
while(hh<=tt)
{
int t=q[hh++];
for(int i=0;i<26;i++)
{
int &p=tr[t][i];
if(!p) p=tr[ne[t]][i];
else
{
ne[p]=tr[ne[t]][i];
q[++tt]=p;
}
}
}
}
int main()
{
scanf("%d",&n);
for(int i=0;i<n;i++)
{
scanf("%s",str);
insert(i);
}
build();
for(int i=idx-1;i>=0;i--) f[ne[q[i]]]+=f[q[i]];
for(int i=0;i<n;i++)
printf("%d\n",f[id[i]]);
return 0;
}