一.个人理解
-
AC自动机是一种字符串匹配的算法,用来将多个模板串t与文本串s匹配。例如:求解文本串中包含了多少个模板串
-
其前置知识是:KMP 和 字典树,貌似不学KMP也没事,只是AC自动机运用到了和 KMP 类似的思想,失配指针。
-
K M P KMP KMP 的 n x t nxt nxt 指针与 a c ac ac 自动机的 f a i l fail fail 指针类似,都是当匹配失败时,回到最长的一个前缀和后缀相同的最长前缀。
前置知识字典树
1.基本概念
- 字典树是一棵包含所有模板串的26叉树。
- 树的”边权“是字符,从根节点出发到任意点的”距离“,即为某一字符串的前缀。
2.建立字典树
int trie[N][26];//字典树trie
int cntword[N];//计算该单词出现次数
int fail[N];//失配指针
int cnt;//动态开点
void insertWords(char *t){
int now=0;
for(int i=0;t[i]!='\0';i++){
int next=t[i]-'a';
if(!trie[now][next])trie[now][next]=++cnt;
now=trie[now][next];
}
cntword[now]++;//当前单词数+1
}
二.算法学习
0.黑箱
- 如果你学完还不明白,你需要记住以下几点
- 1.若 y y y 的 f a i l fail fail 指针连向 x x x,说明 x x x 出现在 y y y 对应的字符串的后缀中。
- 2. b u i l d _ f a i l build\_fail build_fail 后, t r i e trie trie 树会发生变化(原来的边都不会发生变化,只是会多一些边),想要原来的 t r i e trie trie 树可以考虑备份一棵 t r i e trie trie 树。
- 3. b u i l d _ f a i l build\_fail build_fail 前, t r i e trie trie 是一棵树, b u i l d _ f a i l build\_fail build_fail 后, t r i e trie trie 是一个永通路
1.基本理解
- KMP是个一维数组, n x t [ j ] nxt[j] nxt[j] 为模式串 p p p 的第 j j j 个字符的失配指针
- 那么 A C AC AC 自动机,就是在字典树上,求每个位置的失配指针。
2.失配指针是啥,有啥作用???
- 设 w o r d [ i ] word[i] word[i] 表示从根到第 i i i 个结点路径形成的单词
- 如果 t = f a i l [ j ] t=fail[j] t=fail[j] ,则 w o r d [ t ] word[t] word[t] 是 w o r d [ j ] word[j] word[j] 的一个在字典树上存在的最长后缀。很显然 t < j t<j t<j 。
- 若已知一串字符串 s = h e r s s=hers s=hers,则可以通过不断的跳 f a i l fail fail 指针,来计算 h e r s hers hers 的所有后缀串 e r s , r s , s ers,rs,s ers,rs,s出现的次数。
- 那 e r , r er,r er,r呢?在计算 h e r her her 的所有后缀串的时候就算好了!
- 所以 a c ac ac 自动机的过程就是对于每个在字典树上存在的文本串,都求一遍这个文本串的所有后缀串出现的次数。不断加长文本串,如果文本串没有在字典树上出现,就重来,即文本串清 0 0 0 。
- 例如:求 h e r s h i t hershit hershit 在字典树上出现,但 s s s 没有出现。那么ac自动机的过程就是:求 h , h e , h e r , h , h i , h i t h,he,her,h,hi,hit h,he,her,h,hi,hit 出现的次数。
AC自动机的过程
int ac(string s){
int now=0,ans=0;
for(int i=0;s[i]!='\0';i++){//遍历文本串
now=trie[now][s[i]-'a'];//从s[i]开始寻找
//一直向下寻找,直到匹配失败(失败指针指向根或者当前节点已找过)
for(int j=now;j!=0&&cntword[j]!=-1;j=fail[j]){
ans+=cntword[j];
cntword[j]=-1;//防止重复计算
}
}
return ans;
}
3.怎么求fail数组呢???
- 如果求fail数组,即如何求字典树上的每个串的最长后缀子串的位置。
- 我们可以使用bfs层序遍历,很显然,对于一个串的最长后缀字串,其串的深度一定是大于它的最长后缀字串的深度的,bfs的过程中我们会先算深度小的,再算深度大的。
- 考虑两种情况:
- 如果存在这个节点,就让这个节点的失配指针指向其父节点在i失配的指针所指向的点 。
- 如果不存在这个节点,就让这个节点与其父节点在i失配的指针所指向的点,连一条边
void build_fail(){
queue<int>q;
for(int i=0;i<26;i++){
if(trie[0][i]){
fail[trie[0][i]]=0;//第二层的点失配指针指向根节点,跑fail的时候跑到0就婷婷了
q.push(trie[0][i]);//第二层的点扔进队列中
}
}
while(!q.empty()){
int now=q.front();
for(int i=0;i<26;i++){
//如果存在这个节点,就让这个节点的失配指针指向其父节点在i失配的指针所指向的点
if(trie[now][i]){
fail[trie[now][i]]=trie[fail[now]][i];
q.push(trie[now][i]);
}
//如果不存在这个节点,就让这个节点与其父节点在i失配的指针所指向的点,连一条边
else {
trie[now][i]=trie[fail[now]][i];
}
}
q.pop();
}
模板如下
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int trie[N][26];//字典树trie
int cntword[N];//计算该单词出现次数
int fail[N];//失配指针
int cnt;//动态开点
string s;//文本串
string p;//模板串
void insertWords(string p){
int now=0;
for(int i=0;p[i]!='\0';i++){
int next=p[i]-'a';
if(!trie[now][next])trie[now][next]=++cnt;
now=trie[now][next];
}
cntword[now]++;//当前单词数+1
}
void build_fail(){
queue<int>q;//bfs求fail的时候用到
for(int i=0;i<26;i++){
if(trie[0][i]){
fail[trie[0][i]]=0;//第二层的点失配指针指向根节点
q.push(trie[0][i]);//第二层的点扔进队列中
}
}
while(!q.empty()){
int now=q.front();
for(int i=0;i<26;i++){
//如果存在这个节点,就让这个节点的失配指针指向其父节点在i失配的指针所指向的点
if(trie[now][i]){
fail[trie[now][i]]=trie[fail[now]][i];
q.push(trie[now][i]);
}
//如果不存在这个节点,就让这个节点与其父节点在i失配的指针所指向的点,连一条边
else {
trie[now][i]=trie[fail[now]][i];
}
}
q.pop();
}
}
int ac(string s){
int now=0,ans=0;
for(int i=0;s[i]!='\0';i++){//遍历文本串
now=trie[now][s[i]-'a'];//从s[i]开始寻找
//一直向下寻找,直到匹配失败(失败指针指向根或者当前节点已找过)
for(int j=now;j!=0&&cntword[j]!=-1;j=fail[j]){
ans+=cntword[j];
cntword[j]=-1;//防止重复计算
}
}
return ans;
}
int main(){
int T,n;
cin>>T;
while(T--){
memset(cntword,0,sizeof(cntword));
memset(fail,0,sizeof(fail));
memset(trie,0,sizeof(trie));
cin>>n;
for(int i=1;i<=n;i++){
cin>>p;
insertWords(p);
}
build_fail();
cin>>s;
cout<<ac(s)<<endl;
}
return 0;
}
三.算法训练
例题1:模板题
- 题目描述: n n n 个模式串 p p p ,求有多少个模式串在文本 s s s 中出现过。
- 问题分析: 直接跑 a c ac ac 自动机,时间复杂度 O ( ∣ s ∣ + ∑ ∣ p ∣ ) O(|s|+\sum |p|) O(∣s∣+∑∣p∣)
例题2:fail 树的基础应用
- 题目描述: n n n 个模式串 p p p(不保证不同),求每个模式串在文本串 s s s 中分别出现了多少次。
- 问题分析:
- 因为要求每个模板串出现的次数,所以要映射一下模板串在 t r i e trie trie 树上的终止节点
- 现在不能用 c n t w o r d [ j ] = − 1 cntword[j]=-1 cntword[j]=−1 来避免重复计数,因为要求每个模式串 p p p 出现的次数
- 仍然暴力跳 f a i l fail fail 边???
- 若出现 a a a a a a aaaaaa aaaaaa 的数据,会被卡掉
void ac_automaton(string s) {
int now=0;
for(int i=0; s[i]!='\0'; i++) {
now=trie[now][s[i]-'a'];
for(int j=now; j!=0; j=fail[j]) {
num[j]+=cntword[j];
}
}
for(int i=1; i<=n; i++)cout<<num[mark[i]]<<endl;
}
- 方法: 建立 f a i l fail fail 树,将 s s s 在自动机上的前缀对应的结点打上标记 s i z e [ i ] = 1 size[i]=1 size[i]=1,然后求 f a i l fail fail 树的 s i z e [ u ] size[u] size[u] 和 。这样每个模式串匹配的次数就是 s i z e [ u ] size[u] size[u], u u u 表示该模式串对应的结点。
#include<bits/stdc++.h>
using namespace std;
const int N=2e6+10;
int trie[N][26];
int cntword[N];
int fail[N];
int cnt,n;
int size[N];
int mark[N];
int fanmark[N];
string s;
string t;
map<string,int>m;
struct ppp{
int u,v,next;
}e[N*2];
int vex[N],k;
void add(int u,int v){
k++;
e[k].u=u;
e[k].v=v;
e[k].next=vex[u];
vex[u]=k;
}
void insertWords(string t,int pos) {
int now=0;
for(int i=0; t[i]!='\0'; i++) {
int next=t[i]-'a';
if(!trie[now][next])trie[now][next]=++cnt;
now=trie[now][next];
}
cntword[now]++;
mark[pos]=now;
fanmark[now]=pos;
}
void build_fail() {
queue<int>q;
for(int i=0; i<26; i++) {
if(trie[0][i]) {
fail[trie[0][i]]=0;
q.push(trie[0][i]);
}
}
while(!q.empty()) {
int now=q.front();
for(int i=0; i<26; i++) {
if(trie[now][i]) {
fail[trie[now][i]]=trie[fail[now]][i];
q.push(trie[now][i]);
} else {
trie[now][i]=trie[fail[now]][i];
}
}
q.pop();
}
}
void dfs(int u){
for(int i=vex[u];i;i=e[i].next){
int v=e[i].v;
dfs(v);
size[u]+=size[v];
}
}
void ac_automaton(string s) {
int now=0;
for(int i=0; s[i]!='\0'; i++) {
now=trie[now][s[i]-'a'];
size[now]++;
}
for(int i=1;i<=cnt;i++)add(fail[i],i);
dfs(0);
for(int i=1; i<=n; i++)cout<<size[mark[i]]<<endl;
}
int main() {
cin>>n;
for(int i=1; i<=n; i++) {
cin>>t;
if(m[t])mark[i]=mark[m[t]];
else {
m[t]=i;
insertWords(t,i);
}
}
build_fail();
cin>>s;
ac_automaton(s);
return 0;
}