最近在做题的过程中遇到一道题,它要求找出一个字符串中出现了多少中之前给的模式字符串,刚刚看到这个题的时候,
第一反应就是暴力,但是仔细想想,怎么可能是暴力呢,后来就去看了博客,才知道这是ac自动机
1.什么是ac自动机
ac自动机是一种用于多模式匹配的数据结构
2.为什么要使用ac自动机?
因为使用ac自动机会更快的解决多模式匹配的问题
原理
用我自己的话来说,就是,在已经构建的模式串的字典树中进行主串匹配,如果在当前匹配过程中,当前字符可以
匹配,那么就继续沿着这条分支继续匹配,如果不匹配的话,那么就跳到之前我们预处理过的当失败的时候该跳的节点那里去
(他们都叫它失败指针),当然我们要为每一个到过的节点进行标记,如果当前的节点是一个模式串的结尾的话,那么就说明
这个模式串在主串中出现过
按照刚刚我说的,我们要做的工作有
1.构建模式串的字典树:
构建字典树的话比较简单,可以看看代码
2.构建失败指针
为什么要构建失败指针呢?仔细想想,如果当前字符失配了,就说明当前分支的这个字符串就能匹配,就应该跳到下一个模式串,而对于下一个模式串的话,可以有两种情况,一是这个模式串的前缀与当前失配的字符串的后缀有相同的部分,二是没有相同的部分。如果有第一种情况,那么是最好的,我们就跳到那个模式串的公共部分的那个节点之后,这样我们又省去一些时间,而这里的那个节点就被我们称为失败指针。而失败指针是如何构造的呢?这里是采用bfs来构造的,简单的来说:
×如果是第二层的节点,他们的失败指针都是root
×对于接下来的每一层,每个节点都在父节点的失败指针里面找有没有与自己相等的节点,如果有,就指向它,如果没有,就指向root;
3.匹配过程
匹配过程就是如果,遇到不匹配的就跳失败指针
匹配的就继续沿着分支匹配,如果遇到结尾就记录
— 说的有点简单,主要是理解的还不是很升入,后面多加练习了后再来完善
模板:洛古p3830
#include <iostream>
#include <algorithm>
#include <cstring>
#include <queue>
using namespace std;
const int maxn=2e6+5;
int node[maxn],vis[maxn],n,ans;
int trie[maxn][26],fail[maxn],cnt=1;
int insert(string a)
{
int len=a.size();
int id,now=1,i;
for(i=0;i<len;i++)
{
id=a[i]-'a';
if(trie[now][id])now=trie[now][id];
else trie[now][id]=++cnt,now=cnt;
}
return cnt;
}
void create()
{
queue<int>q;
q.push(1);
int now,p,v,i;
while(!q.empty())
{
now=q.front();q.pop();
for(i=0;i<26;i++)
{
if(trie[now][i]==0)continue;
v=trie[now][i],p=fail[now];
while(p&&!trie[p][i])p=fail[p];//为什么要这样写?可以自己思考一下
if(!p)fail[v]=1;
else fail[v]=trie[p][i];
q.push(v);
}
}
}
void work(string a)
{
int len=a.size(),now=1,id,p,i;
for(i=0;i<len;i++)
{
id=a[i]-'a',vis[now]=1;
while(now&&!trie[now][id])now=fail[now];
if(now)now=trie[now][id];else now=1;
p=fail[now];
while(p&&!vis[p])vis[p]=1,p=fail[p];
}
vis[now]=1;
}
int main()
{
string s;
int i,j,k,ans=0;
cin>>n;
for(i=0;i<n;i++)cin>>s,node[i]=insert(s);
create();cin>>s,work(s);
for(i=0;i<n;i++)if(vis[node[i]])++ans;
cout<<ans<<endl;
return 0;
}