前置芝士
引入
来看一道题目:(洛谷 P3808 【模板】AC自动机(简单版))
给定 n n n 个模式串 s i s_i si 和一个文本串 t t t,求有多少个不同的模式串在文本串里出现过。
你想到了什么?
KMP?KMP用来处理单模匹配问题有奇效,但是解决这道题么。。。
这就要用到一种神奇 难 的算法:AC自动机了!(充满诱惑力的名字)
AC自动机
Aho-Corasick automaton,该算法在1975年产生于贝尔实验室,是著名的多模匹配算法。——百度百科
首先我们将所有模式串存入Trie树,存入的方式与单独的Trie一模一样。
比如我们加入"ask",“skex”,“bhd”,“ska”:
然后,我们开始跑AC自动机了!其实,AC自动机就是在Trie树上做KMP。
我们来复习一下KMP的做法,首先要处理nxt。nxt在KMP中的意义是什么呢?是模式串的每个子串的最长公共前后缀。那nxt有什么作用呢?可以避免失配后的从头匹配。举个例子:
我们有一个模式串"ababa",我们处理出它的nxt:
a | b | a | b | a |
---|---|---|---|---|
0 | 0 | 0 | 1 | 2 |
我们放到文本串"ababcababa"中匹配:
当匹配到c时失配,而这时c前面的abab已经匹配过了,这时我们就可以跳到nxt上继续匹配:
仍然失配,跳到nxt:
失配,跳:
匹配成功,结束。
为什么可以这样呢?因为我们每次失配前,都已经匹配过一段,如果有与这一段相同的部分就不用再匹配了,也就是我们的跳nxt。
好了,KMP复习完了,我们可以发现,进行多模匹配时,也会有两个串之间相同的部分,所以,我们可以在Trie树上使用nxt。
那么,Trie树上的nxt有什么作用呢?我们来看一看:
假设我们已经匹配完了ask:
按照以前的做法,我们应该回到根,重新匹配。然而,我们可以发现,ask中的"sk"与skex以及ska中的"sk"相同!我们可以选择直接跳过去:
这样,我们就不用再匹配依次"sk"了!
那这么神奇的功能是怎么实现的呢?
神奇海螺告诉我们,只需要使用bfs!
首先,我们让 第 一 层 {\color{red}{第一层}} 第一层的节点的nxt全部指向根:
开始遍历
第
二
层
{\color{limegreen}{第二层}}
第二层。对于节点s,我们让它的nxt指向它的父亲节点的nxt的具有相同字母的儿子:
第 二 层 {\color{limegreen}{第二层}} 第二层遍历后如下:
我们以刚才的规律,继续遍历
第
三
层
{\color{gold}{第三层}}
第三层:
第 四 层 {\color{blue}{第四层}} 第四层:
(好像有一点点乱)
这样,我们nxt就处理好了。我们现在可以把文本串放上去跑了:
以"askex"为例,我们匹配到k时,发现没有e了,就跳到k的nxt上继续匹配,一直匹配到x,结束。
但是,我们跳到nxt上之后,如果这个节点仍然不存在呢?我们要继续跳nxt,一直跳到存在或到达根节点为止?
是不是觉得像这样一直跳太麻烦了,而且消耗时间。有没有办法优化一下呢?
我们可以发现,对于任意一个节点,它的nxt一定比它要先处理到(因为它们不在一层),我们是不是可以在bfs的时候将每个节点的nxt直接接到最后的nxt上呢?
经过 抄袭 思考后,我们发现,对于所有不存在的节点,我们才会去跳nxt(因为不存在表示当前失配),而对于不存在的节点,它在Trie树上是没有记录的。所以我们可以将Trie树上一个节点的不存在的儿子给标记为这个节点的nxt的儿子(强行认爹)。这时,我们修改了Trie树,所以Trie树已经不再是树了,而是一张图,我们称之为Trie图。
至此,我们的Trie树在神奇海螺的指引下强行认爹,成功进化。进化成的Trie图就可以用来跑文本串了!
完整代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;
int n,tot=1;
string s,str;
struct trie
{
int b;
int ch[26];
int nxt;
}
t[500010];
void insert(string s)
{
int p=1;
for(int i=0;i<s.length();i++)
{
if(!t[p].ch[s[i]-'a'])
t[p].ch[s[i]-'a']=++tot;
p=t[p].ch[s[i]-'a'];
}
t[p].b++;
}
void bfs()//处理nxt
{
queue<int> q;
for(int i=0;i<26;i++)
t[0].ch[i]=1;//将0节点接到1,便于跳nxt
q.push(1);
t[1].nxt=0;
while(!q.empty())
{
int u=q.front();
q.pop();
int f=t[u].nxt;
for(int i=0;i<26;i++)
{
int v=t[u].ch[i];
if(!v)//若儿子不存在
t[u].ch[i]=t[f].ch[i];//强行认爹
else
{
t[v].nxt=t[f].ch[i];//储存nxt
q.push(v);//入队,继续bfs
}
}
}
}
int search(string s)
{
int p=1,ans=0;
for(int i=0;i<s.length();i++)
{
int k=t[p].ch[s[i]-'a'];
while(k>1&&t[k].b!=-1)//不断匹配,直到匹配到根
{
ans+=t[k].b;
t[k].b=-1;
k=t[k].nxt;
}
p=t[p].ch[s[i]-'a'];
}
return ans;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
cin>>str;
insert(str);
}
bfs();
cin>>s;
printf("%d\n",search(s));
return 0;
}