AC自动机——杨子曰算法
”哦,什么,这个算法可以自动AC题目?“
”呵呵,你想多了……“
在阅读此文章前,请先确保掌握字典树和KMP算法,你也可以阅读:
字典树(trie)——杨子曰算法
KMP——杨子曰算法
字典树和KMP都是用来处理字符串的问题的,我们用KMP可以解决类似yzy在yzyyzyzyzyblockyzyzy出现了几次的问题,但如果有一天有一个人问你:yz,yzy,bl,yzyb在yzyblockyzy出现了几个时,你想跑4遍KMP吗?
海枯石烂了也出不了答案,于是有一个牛逼的玩意诞生了——AC自动机
AC自动机的核心想法其实只有一句话:在trie树上做KMP(手动一脸懵逼)
黑喂狗:
我们以如下样例为例:
模式串:
abf
abc
abcd
cda
de
df
文本串:
abcde
1.用模式串建一棵字典树
void build(string s){
int k=0;
for (int i=0;s[i];i++){
int p=s[i]-'a';
if (!tr[k][p]) tr[k][p]=++sum;
k=tr[k][p];
}
num[k]++;
}
它会长这样(蓝色节点表示一个单词的结尾):
接下来我们用文本串abcde试着匹配看看,会发生什么?
首先从根节点出发,哦,成功找到a,于是走到a,又找到了b,接着又来到了c,嗯,到一个单词的结尾了,ans++,美滋滋
接着找d,ans再++,然后我们想找e,但它没有,怎么办呢?说明从第一个字符a开始的匹配已经没有了,只好回到根节点,找第二个字符b,呃,trie里没有b,那就看第三个字符c,又找d,发现没有e,哎,又要重新开始……
有没有发现效率巨低!原因就是当我们匹配失败时,或当前匹配完成时,又得换一个开头从根节点重来
杨子曰:思想是可以转移的
这时候,你有没有想到KMP,它就充分利用了当前匹配的信息,不用从头开始,So,我们能不能也把它用在trie上呢?
比方说刚才已经再最下面的那个d上了,就说明文本串完美地匹配了这条路径上的abcd,于是乎,我能不能直接跳到这里:
因为它们都有公共的子串cd,我们不妨称这种到达这个节点匹配失败时指向下一个应该去的节点的指针叫fail指针
fail[i]指向的其实就是trie树上i这条路径后缀和trie树上其他路径前缀的最长匹配的结尾字符(你或许晕了,但这句话不是很重要,如果你还是想理解的话,我用上面那个例子模拟一下,从根节点到当前匹配到d上的串是abcd,而从根节点出发还有一条路径叫做cda,这两个串的前缀和后缀最长匹配是ad,So,要把abcd的d指向cda的d)
也就是说,当我们已经走到节点i了,那么走到fail[i]也一定不成问题,就不用重新走一遍了
有没有发现这与KMP有几分形似,妙啊!妙啊!
2.找出每个节点的fail指针
它会长这样:
除标出来的指针外,其他指针都是指向根节点的(因为没任何路径的有前缀能与它们的后缀相匹配)
那么,我们如何求出fail指针捏?
对于fail[i]指向的是i节点的爸爸的fail指针所指向结点的儿子中与i对应的节点(←这句话灰常重要,一定要多看两遍)
又有人晕了,我们Look At the 图:
比方说我们现在要求fail[6],那么就先找到它的爸爸5,再找到fail[5]也就是7,在节点7的儿子中找到d也就是8,那么此时fail[6]也就是8了
这样做无非就是接着前面不断拓展
然后你会发现,fail必须从上往下算,怎么办呢?——BFS
第一层(也就是根节点下面的那一层)的fail指针都是指向根节点,于是就从它们开始一层一层往下求fail
void getfail(){
queue <int>q;
for (int i=0;i<26;i++){
if (tr[0][i]){
fail[tr[0][i]]=0;
q.push(tr[0][i]);
}
}
while(!q.empty()){
int k=q.front();
q.pop();
for (int i=0;i<26;i++){
if (tr[k][i]){
fail[tr[k][i]]=tr[fail[k]][i];
q.push(tr[k][i]);
}
else tr[k][i]=tr[fail[k]][i];//如果k根本就没有这个儿子,就说明走到它会匹配失败,那我们就直接指向fail指针
}
}
}
3.匹配
这样一来,匹配就变得炒鸡简单,我们就用上面的样例来模拟一下好了
文本串:abcdef
首先我们从根节点出发
发现有a,于是来到a
再来到b
又到c
是一个单词的结尾了,ans++,再到d
又是结尾,ans++,我们想要找e,但是它木有,So,我们沿着fail指针来到这:
再看这个d的儿子,发现又没有e,于是再沿着fail走
啊!这个d终于有儿子e了!美滋滋,ans++
再看e的儿子,又没有f
跳到它的fail,也就是根节点
再找f,发现依然没有
至此文本串被我们扫完了
终于,匹配结束!
注意一下:如果我们能找到当前的节点,那就说明从这个节点开始的这一条fail指针构成的链上的点它都是可以到的,所以我们要把整条链对答案的贡献都加上
int find(string s){
int k=0,ans=0;
for (int i=0;s[i];i++){
int p=s[i]-'a';
k=tr[k][p];
for (int j=k;j && num[j]!=-1;j=fail[j]){
ans+=num[j];
num[j]=-1;
}
}
return ans;
}
OK,完事
c++代码(洛谷P3808):
#include<bits/stdc++.h>
using namespace std;
const int maxn=500005;
int tr[maxn][30],num[maxn],fail[maxn],sum=0;
void build(string s){
int k=0;
for (int i=0;s[i];i++){
int p=s[i]-'a';
if (!tr[k][p]) tr[k][p]=++sum;
k=tr[k][p];
}
num[k]++;
}
void getfail(){
queue <int>q;
for (int i=0;i<26;i++){
if (tr[0][i]){
fail[tr[0][i]]=0;
q.push(tr[0][i]);
}
}
while(!q.empty()){
int k=q.front();
q.pop();
for (int i=0;i<26;i++){
if (tr[k][i]){
fail[tr[k][i]]=tr[fail[k]][i];
q.push(tr[k][i]);
}
else tr[k][i]=tr[fail[k]][i];
}
}
}
int find(string s){
int k=0,ans=0;
for (int i=0;s[i];i++){
int p=s[i]-'a';
k=tr[k][p];
for (int j=k;j && num[j]!=-1;j=fail[j]){
ans+=num[j];
num[j]=-1;
}
}
return ans;
}
int main(){
int n;
scanf("%d\n",&n);
for (int i=1;i<=n;i++){
string s;
cin>>s;
build(s);
}
getfail();
string s;
cin>>s;
cout<<find(s);
return 0;
}
于HG机房