AC自动机学习笔记
来补字符串的知识点了,,
ac自动机可以用来解决这样的问题:
给定多个模式串和一个长文本,求每个模式串在文本中出现的次数。
算法的核心是在trie树上建立fail边,每次失配的时候沿着fail边跳到另外的节点上,fail边建立当且仅当连向的节点在trie树上的前缀是原来节点在trie树上的前缀的后缀。写起来是这样的:
int fl=ac[u].fail;
for(int i=0;i<=26;i++){
if(!ac[u].vis[i]){ac[u].vis[i]=ac[fl].vis[i];continue;}
ac[ac[u].vis[i]].fail=ac[fl].vis[i];
rd[ac[ac[u].vis[i]].fail]++;
k1.push(ac[u].vis[i]);
}
那么如果没有向下的节点了,则父节点向某字母子节点的路连向父节点的fail节点的对应字母子节点;
如果有,则子节点创建一条fail路连向父节点的fail节点的对应字母子节点。
最简单的ac自动机是暴力跳fail的,最差情况下(模式串+文本串是aaaaaaaa这样的////)可以被卡成O(模式串长度*文本串长度),这是不可接受的。
优化方法是:在匹配过的节点上打上标记,然后除非失配时跳向对应的fail节点,其余时候先不跳fail了。
等到匹配了一遍之后我们得到了一颗fail树,树上的一些节点打上了标记,简单版的跳fail操作实际上就是在打上的标记的点上继续沿着fail树向下加上贡献。那么这个时候我们只需要拓扑排序或者bfs一遍把打上标记的点的贡献更新到所有节点上即可。
例题:洛谷P5357
#include <bits/stdc++.h>
using namespace std;
const int maxn=2e6+10;
struct aczdj{
int fail,vis[30],flag,ans1;
}ac[maxn];
int cnt=1,n;
int rd[maxn],ans[200010],mp[maxn];
queue<int> k1;
void build(string &now,int tp){
int now1=now.length(),now2=1;
for(int i=0;i<now1;i++){
int v=now[i]-'a';
if(!ac[now2].vis[v]) ac[now2].vis[v]=++cnt;
now2=ac[now2].vis[v];
}
if(!ac[now2].flag) ac[now2].flag=tp;
mp[tp]=ac[now2].flag;
}
void buildfail(){
for(int i=0;i<26;i++) ac[0].vis[i]=1;////
k1.push(1);
while(!k1.empty()){
int u=k1.front();k1.pop();
int fl=ac[u].fail;
for(int i=0;i<26;i++){
if(!ac[u].vis[i]){ac[u].vis[i]=ac[fl].vis[i];continue;}
ac[ac[u].vis[i]].fail=ac[fl].vis[i];
rd[ac[ac[u].vis[i]].fail]++;
k1.push(ac[u].vis[i]);
}
}
}
void topo(){
for(int i=1;i<=cnt;i++){
if(rd[i]==0) k1.push(i);
}
while(!k1.empty()){
int tmp=k1.front();k1.pop();
ans[ac[tmp].flag]=ac[tmp].ans1;
int v=ac[tmp].fail;
rd[v]--;
ac[v].ans1+=ac[tmp].ans1;
if(rd[v]==0) k1.push(v);
}
}
void acque(string &now){
int now1=now.length(),now2=1;
for(int i=0;i<now1;i++){
now2=ac[now2].vis[now[i]-'a'];
ac[now2].ans1++;
}
}
signed main(){
cin>>n;
string yuan,all;
for(int i=1;i<=n;i++){
cin>>yuan;
build(yuan,i);
}
buildfail();
cin>>all;
acque(all);
topo();
for(int i=1;i<=n;i++) cout<<ans[mp[i]]<<endl;
}

浙公网安备 33010602011771号