字符串 _ AC自动机
概述
算法作用:
在O(n)
的复杂度 求出多个匹配串 出现在 模式串的 哪些地方 出现次数。
算法核心:
自动机的和kmp 类似,关键是next指针
构建next指针
next指针:状态 u 的 next 指针指向另一个状态 v, 当且仅当 v 是 u 的最长后缀。
即: next 指针是指向所有模式串的前缀中匹配当前状态的最长后缀的状态。
非平凡的后缀:除了自身以外的后缀。
上面的后缀都是非平凡的。
Trie 中的结点表示的是某个模式串的前缀。我们在后文也将其称作状态。
一个结点表示一个状态,Trie 的边就是状态的转移。
如图:
如何求next呢? 和kmp类似:
如果节点 u 的父亲节点是fa,且fa通过字符 c 的边指向 u ;父亲节点fa的next指向k。
如果我们要找u的next指针,那么我们就看k的子节点
-
如果 \(trie[k][c]\) 存在:u 的 \(next\) 就指向 \(trie[k][c]\)
-
如果 \(trie[k][c]\) 不存在:就 while(u=next[u]),直到找到相同的或者根节点为止。将u 的 \(next\) 就指向 \(trie[k][c]\)
代码
我们可以发现我们是用前面层的信息去 更新 新的一层的信息,所以next需要一层一层的获取,因此用bfs。
void build(){
queue<int> q;
//从根的所有儿子开始搜索
for(int i=0;i<26;i++){
if(tr[0][i])
q.push(tr[0][i]);
}
while(q.size()){
int t=q.front();
q.pop();
for(int i=0;i<26;i++){
int c=tr[t][i];
if(!c) continue;//如果节点不存在continue
int j=ne[t];
//如果next指针指到的位置的儿子不存在;
//这个字符那么就一直往回找直到找到为止
while(j && !tr[j][i]) j=ne[j];
//next数组等于找到的节点j的next
if(tr[j][i]) j=tr[j][i];
ne[c]=j;
q.push(c);
}
}
}
匹配过程
和kmp类似:查找过程和构建过程差不多。
注意几点:
-
如果一个字符串匹配成功,那么他的后缀也一定可以匹配成功。
比如说:成功匹配到she:那么所有she的后缀(he,e)也一定可以匹配 -
为了避免重复计算,我们将经过的cnt[u]设为0。
cnt数组是有多少以此节点为结尾的匹配串
代码:
cin>>str;
int ans=0;
for(int i=0,j=0;str[i];i++){
int t=str[i]-'a';
//
while(j && !tr[j][t]) j=ne[j];
if(tr[j][t]) j=tr[j][t];
int p=j;
//回溯匹配串的后缀
while(p){
ans+=cnt[p];
cnt[p]=0;//cnt数组是有多少以此节点为结尾的匹配串
p=ne[p];
}
}
cout<<ans<<endl;
Trie图优化
我们可以发现代码上面里面有个while循环,虽然while还是常数,但是我们希望可以优化掉。
证明while是常数
待补
优化思路:
在没有匹配时 把while循环多次跳 优化为 直接跳到ne指针最终跳到的位置。从而去掉while循环。
优化方式: 直接记住吧
每次取出队首的结点u (fail[u]在之前的 BFS 过程中已求得),然后遍历字符集u的各个子节点:
- 如果 \(trans[u][i]\) 存在,我们就将 \(trans[u][i]\) 的next 指针赋值为 \(trans[next[u]][i]\)
- 否则,令trans[u][i] 指向 \(trans[next[u]][i]\) 的状态。
oiwikl的解释
严格鸽 的解释
类似于并查集的路径压缩,我们可以建成Trie图。
(因为会使Trie原本的DAG结构出现环。)
构建代码:
void build(){
queue<int> q;
//从根的所有儿子开始搜索
for(int i=0;i<26;i++){
if(tr[0][i])
q.push(tr[0][i]);
}
while(q.size()){
int t=q.front();
q.pop();
for(int i=0;i<26;i++){
int p=tr[t][i];
if(!p) tr[t][i]=tr[ne[t]][i];
else{
ne[p]=tr[ne[t]][i];
q.push(p);
}
}
}
}
遍历时只需要直接访问即可
int ans=0;
for(int i=0,j=0;str[i];i++){
int t=str[i]-'a';
j=tr[j][t];
int p=j;
while(p){
ans+=cnt[p];
cnt[p]=0;
p=ne[p];
}
}
模板代码
#include <bits/stdc++.h>
using namespace std;
const int N=10010,S=55;
int n;
string str;
int tr[N*S][26],cnt[N*S],idx;
int ne[N*S];
void insert(){
int p=0;
for(int i=0;i<str[i];i++)
{
int t=str[i]-'a';
if(!tr[p][t]) tr[p][t]=++idx;
p=tr[p][t];
}
cnt[p]++;
}
void build(){
queue<int> q;
for(int i=0;i<26;i++){
if(tr[0][i])
q.push(tr[0][i]);
}
while(q.size()){
int t=q.front();
q.pop();
for(int i=0;i<26;i++){
int p=tr[t][i];
if(!p) tr[t][i]=tr[ne[t]][i];
else{
ne[p]=tr[ne[t]][i];
q.push(p);
}
}
}
}
void solve(){
memset(tr,0,sizeof tr);
memset(cnt,0,sizeof cnt);
memset(ne,0,sizeof ne);
idx=0;
cin>>n;
for(int i=0;i<n;i++){
cin>>str;
insert();
}
build();
cin>>str;
int ans=0;
for(int i=0,j=0;str[i];i++){
int t=str[i]-'a';
j=tr[j][t];
int p=j;
while(p){
ans+=cnt[p];
cnt[p]=0;
p=ne[p];
}
}
cout<<ans<<endl;
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int t;cin>>t;
while(t--)
solve();
return 0;
}