字符串 _ 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的子节点
-
如果 存在:u 的 就指向
-
如果 不存在:就 while(u=next[u]),直到找到相同的或者根节点为止。将u 的 就指向
代码
我们可以发现我们是用前面层的信息去 更新 新的一层的信息,所以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的各个子节点:
- 如果 存在,我们就将 的next 指针赋值为
- 否则,令trans[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; }
应用
本文作者:kingwzun
本文链接:https://www.cnblogs.com/kingwz/p/16565077.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步