这个算法适用于多个字符串匹配,简单来说就是树上KMP。
以一道例题来解释洛谷P3808
题目描述
给定n个模式串si和一个文本串t求有多少个不同的模式串在文本串里出现过。两个模式串不同当且仅当他们编号不同。
输入格式
- 第一行是一个整数,表示模式串的个数n。
- 第2到第(n+1) 行,每行一个字符串第 (i+1) 行的字符串表示编号为i的模式串si。
- 最后一行是一个字符串,表示文本串t。
输出格式
输出一行一个整数表示答案。
思路
这是洛谷上的模板题,求模式串在文本串中出现了多少次。如果不用AC自动机,直接暴力去匹配显然会T飞了。那么,怎么优化呢?
为了便于操作,我们可以把模式串全扔到trie树上(trie树不会的可以看这个)
类似于KMP,在每一次失配时不重头开始匹配,而是跳到合适的位置。怎么判断这个合适的位置在哪呢?类似于KMP的next数组,这里引入fail指针。那么它的原理是什么?我们在匹配字符串时,知道的信息只有我们匹配过的部分,所以fail指针就是利用我们匹配过的部分进行判断如果在当前位置失配要跳到哪里。这里给出一个图片,便于理解fail指针。
显然,指向的部分是当前匹配过的部分的后缀。那么,如何预处理呢?
我们可以利用广搜来处理,把每一个需要处理的节点入队,代码如下。
void getfail()
{
queue<int>q;
for(int i=0;i<=25;i++)
{
if(trie[0][i]) q.push(trie[0][i]);
}
while(!q.empty())
{
int u=q.front();
q.pop();
for(int i=0;i<=25;i++)
{
if(trie[u][i])
{
fail[trie[u][i]]=trie[fail[u]][i];
q.push(trie[u][i]);
}
else
{
trie[u][i]=trie[fail[u]][i];//路径压缩
}
}
}
}
到这里,AC自动机的核心,fail指针部分就说完了,之后再进行匹配就好了.
下面附上上面给出的那道题的AC代码,不理解的可以再看一下。
#include<bits/stdc++.h>
using namespace std;
int n,m,k[1000005][27],ans,cnt,mark[1000005],fail[1000006];
string a[1000005],s;
void add(string s)//build trie tree
{
int u=0,l=s.size();
for(int i=0;i<l;i++)
{
int x=s[i]-'a';
if(k[u][x]==0) k[u][x]=++cnt;
u=k[u][x];
}
mark[u]++;
}
void getfail()//figure out the fail
{
queue <int> q;
for(int i=0;i<=25;i++)
{
if(k[0][i]) q.push(k[0][i]);//fail[ch[0][i]]=0;//BFs in
}
while(!q.empty())//BFS
{
int u=q.front();
q.pop();
for(int i=0;i<=25;i++)
{
int v=k[u][i];//lazy
if(v)
{
fail[v]=k[fail[u]][i];//jump to fail例子:a调到它爹的fail的a
q.push(v); //add
}
else
{
k[u][i]=k[fail[u]][i];//压缩路径
}
}
}
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i],add(a[i]);
cin>>s;
int ls=s.size();
int u=0,c;
getfail();
for(int i=0;i<ls;i++)
{
c=s[i]-'a';
u=k[u][c];
for(int p=u;p&&mark[p]!=-1;p=fail[p])
{
ans+=mark[p];
mark[p]=-1;
}
}
cout<<ans<<endl;
return 0;
}
PS:AC自动机是一个值得欣赏的算法,而自动AC机是违法的(
如有错误求大佬指出,感激不尽