2.5AC自动机

2.5AC自动机

是什么

  • AC自动机,著名的多模式串匹配算法之一
  • 简单来说,AC自动机就是在Trie树上进行KMP
  • 通常用于解决在m给字符中查询n个单词出现的次数

实现(以ybtoj模板题为例)

  1. 借助Trie树进行建树
void add(char *s){
    int u=1,len=strlen(s);
    for(int i=0;i<len;i++){
        int c=s[i]-'a';
        if(!ch[u][c]){
            ch[u][c]=++cnt;
            memset(ch[cnt],0,sizeof(ch[cnt]));//点cnt的c指针指向节点在上一次已经被更新,需要先清零
        }
        u=ch[u][c];
    }
    ++bo[u];
}
  1. 预处理fail指针
  • 定义nxt指针:记root到i为字符串A,root到j为字符串T,nxt[i]=j表示T是Trie树上A的最长后缀
  • 如何构建nxt数组
    • 一个显然的性质:nxt[u]的深度一定小于u
    • 因此,可以用BFS保证nxt[u]在u之前被遍历,并用u更新子节点的nxt
    1. 记u的字节点为v,u->v为C指针
    2. 对u节点不断跳nxt,直到nxt指向子节点w的指针也为C指针时停止,将nxt[v]=w
    3. 特殊处理:建立0号节点,使nxt[root]=1,并将0号节点的所以指针指向root,这样就可以在不加特判的情况下使没有后缀的节点都指向root
void bfs(){//失配指针
    for(int i=0;i<26;i++) ch[0][i]=1;//建立0号节点
    queue<int>q;
    q.push(1);
    nxt[1]=0;
    while(!q.empty()){
        int u=q.front();
        q.pop();
        for(int i=0;i<26;i++){
            if(!ch[u][i])//将指向空的节点指向失配节点,方便else中操作
              ch[u][i]=ch[nxt[u]][i];//相当于并查集中的路径压缩的思想对nxt进行缩短
            else{
                nxt[ch[u][i]]=ch[nxt[u]][i];//用u节点更新子节点ch[u][i]的nxt
                q.push(ch[u][i]);
            }
        }
    }
}
  1. 仿照KMP对文章进行查询
  • 由于我们在预处理Fail指针时就已经处理好每个节点该往哪里跳了(if(!ch[u][i])ch[u][i]=ch[nxt[u]][i];//相当于并查集中的路径压缩的思想对nxt进行缩短),所以不需要考虑失配情况,直接在Trie树上遍历nxt节点,统计遇到的结束标记的数量即可
void find(char *s){
    int u=1,len=strlen(s),k;
    for(int i=0;i<len;i++){
        int c=s[i]-'a';
        k=ch[u][c];
        while(k>1&&bo[k]!=-1){//不断跳nxt到头,统计经过的结束标记数
            ans+=bo[k];
            bo[k]=-1;//!!!清空,进行标记,放置重复统计
            k=nxt[k];
        }
        u=ch[u][c];
    }
}

注意应对多组测试数据的清零操作

例题

例题1:单词统计(ybtoj模板)

接上,代码如下

#include<bits/stdc++.h>
using namespace std;
const int N = 1e6+10;
const int M = 1e6+10;
const int C = 30;
int cnt=1,ch[N][C],bo[N],n,nxt[N],t,ans=0,que[N];//nxt:失配指针  bo:结束节点
char s[M];
void add(char *s){
    int u=1,len=strlen(s);
    for(int i=0;i<len;i++){
        int c=s[i]-'a';
        if(!ch[u][c]){
            ch[u][c]=++cnt;
            memset(ch[cnt],0,sizeof(ch[cnt]));//点cnt的c指针指向节点在上一次已经被更新,需要先清零
        }
        u=ch[u][c];
    }
    ++bo[u];
}

void bfs(){//失配指针
    for(int i=0;i<26;i++) ch[0][i]=1;//建立0号节点
    queue<int>q;
    q.push(1);
    nxt[1]=0;
    while(!q.empty()){
        int u=q.front();
        q.pop();
        for(int i=0;i<26;i++){
            if(!ch[u][i])ch[u][i]=ch[nxt[u]][i];//相当于并查集中的路径压缩的思想对nxt进行缩短
            else{
                nxt[ch[u][i]]=ch[nxt[u]][i];//用u节点更新子节点ch[u][i]的nxt
                q.push(ch[u][i]);
            }
        }
    }
}

void find(char *s){
    int u=1,len=strlen(s),k;
    for(int i=0;i<len;i++){
        int c=s[i]-'a';
        k=ch[u][c];
        while(k>1&&bo[k]!=-1){//不断跳nxt到头,统计经过的结束标记数
            ans+=bo[k];
            bo[k]=-1;//!!!清空,进行标记,放置重复统计
            k=nxt[k];
        }
        u=ch[u][c];
    }
}

int main(){
    scanf("%d",&t);
    while(t--){
        memset(nxt,0,sizeof(nxt));
        memset(bo,0,sizeof(bo));
        cnt=1;
        ans=0;
        for(int i=0;i<26;i++){//初始化0号与1号节点
            ch[0][i]=1;
            ch[1][i]=0;
        }
        scanf("%d",&n);
        for(int i=1;i<=n;i++){
            scanf("%s",s);
            add(s);
        }
        bfs();
        scanf("%s",s);
        find(s);
        printf("%d\n",ans);
    }
    return 0;
}

洛谷模板P3808 【模板】AC 自动机(简单版)
删去多组数据的特殊处理即可

#include<bits/stdc++.h>
using namespace std;
const int N = 1e6+10;
const int M = 1e6+10;
const int C = 30;
int cnt=1,ch[N][C],bo[N],n,nxt[N],t,ans=0,que[N];//nxt:失配指针
char s[M];
void add(char *s){
   int u=1,len=strlen(s);
   for(int i=0;i<len;i++){
       int c=s[i]-'a';
       if(!ch[u][c]){
           ch[u][c]=++cnt;
           memset(ch[cnt],0,sizeof(ch[cnt]));//点cnt的c指针指向节点在上一次已经被更新,需要先清零
       }
       u=ch[u][c];
   }
   ++bo[u];
}

void bfs(){//失配指针
   for(int i=0;i<26;i++) ch[0][i]=1;//建立0号节点
   queue< int >q;
   q.push(1);
   nxt[1]=0;
   while(!q.empty()){
       int u=q.front();
       q.pop();
       for(int i=0;i<26;i++){
           if(!ch[u][i])ch[u][i]=ch[nxt[u]][i];
           else{
               nxt[ch[u][i]]=ch[nxt[u]][i];//用u节点更新子节点ch[u][i]的nxt
               q.push(ch[u][i]);
           }
       }
   }
}

void find(char *s){
   int u=1,len=strlen(s),k;
   for(int i=0;i<len;i++){
       int c=s[i]-'a';
       k=ch[u][c];
       while(k>1&&bo[k]!=-1){//不断跳nxt到头,统计经过的结束标记数
           ans+=bo[k];
           bo[k]=-1;//!!!清空
           k=nxt[k];
       }
       u=ch[u][c];
   }
}

int main(){
   scanf("%d",&n);
   for(int i=1;i<=n;i++){
       scanf("%s",s);
       add(s);
   }
   bfs();
   scanf("%s",s);
   find(s);
   printf("%d\n",ans);
   cnt=1;
   return 0;
}

例题2:单词频率

  1. 思路:
  • 首先,将n个单词存入Trie树,方便统计
  • 考虑如何统计每个单词在文章中出现的次数:
  • 首先分析一个简单的例子:
3
aaa
aa
a
  • 对于a,它只有a一个前缀,该前缀也只有一个后缀a,该前缀的后缀与第三个字符串相同,即第三个字符串出现一次
  • 对于aa,分别分析a,aa两个前缀
    1. a:只有一个后缀该前缀的后缀与第三个字符串相同,即第三个字符串又出现一次
    1. aa:它的一个后缀a与第三个字符串相同,即第三个字符串又又出现一次;它的另一个后缀aa与第二个字符串相同,即第二个字符串出现一次
  • 对于aaa,分别分析a,aa,aaa三个前缀
    1. 前两个前缀与上一种情况分别相同,故第三个字符串又又又又出现了且第二个字符串又出现一次
    1. aaa:这个前缀的一个后缀aaa与第一个字符串相同,故第一个字符串出现一次;另一个后缀aa与第二个字符串相同,故第二个字符串又又出现一次;第三个后缀a与第三个字符串相同,故第三个字符串又又又又又出现一次
      综上所述,a出现6次,aa出现3次,aaa出现1次
  • 我们发现:
    • 上述过程可以推广到全部字符串求解
    • 上述过程中,由于AC自动机的性质,巧了,求前缀恰好相当于Trie树从root dfs遍历的过程,而求后缀的过程恰好相当于在树上不断跳失配指针的过程
  • 因此,我们可以先处理好AC自动机,在建树的过程中新建一个sum数组,记录每个点在文章中出现的次数(即在访问到时将该点sum++),在更新fail指针时记录经过节点,对这些节点进行后缀和统计即可
  • 所以这也是个板子
void bfs(){
    for(int i=0;i<=25;i++){
        tr[0][i]=1;
    }
    que[1]=1;
    nxt[1]=0;
    for(int q1=1,q2=1;q1<=q2;q1++){
        int u=que[q1];
        for(int i=0;i<=25;i++){
            if(!tr[u][i])tr[u][i]=tr[nxt[u]][i];//将指向空的节点指向失配节点,方便else中操作
            else{
                que[++q2]=tr[u][i];
                nxt[tr[u][i]]=tr[nxt[u]][i];//失配指针指向u节点的nxt节点的i指针指向的节点
            }
        }
    }
    //从后向前遍历,更新子树中次数和
    //操作方法为先将sum数组复制到sum1中,对sum1进行“后缀和”
    for(int i=cnt;i>=0;i--)sum1[que[i]]=sum[que[i]];
    for(int i=cnt;i>=0;i--)sum1[nxt[que[i]]]+=sum1[que[i]];
}
  1. 代码:
#include<bits/stdc++.h>
using namespace std;

const int N = 5200005;
int n,tr[N][30],nxt[N],pos[N],cnt=1/*记录Trie树的节点数*/,sum[N]/*sum[i]记录以root->i为前缀的字符串的数量*/,sum1[N]/*sum的“后缀和”*/,tot/*记录字符串个数*/,que[N];
char ch[N];
void add(char *s){
    int u=1,len=strlen(s);
    for(int i=0;i<len;i++){
        int C=s[i]-'a';
        if(!tr[u][C]) tr[u][C]=++cnt;
        u=tr[u][C];
        sum[u]++;//沿途节点sum++
    }
    pos[++tot]=u;
}

void bfs(){
    for(int i=0;i<=25;i++){
        tr[0][i]=1;
    }
    que[1]=1;
    nxt[1]=0;
    for(int q1=1,q2=1;q1<=q2;q1++){
        int u=que[q1];
        for(int i=0;i<=25;i++){
            if(!tr[u][i])tr[u][i]=tr[nxt[u]][i];//将指向空的节点指向失配节点,方便else中操作
            else{
                que[++q2]=tr[u][i];
                nxt[tr[u][i]]=tr[nxt[u]][i];//失配指针指向u节点的nxt节点的i指针指向的节点
            }
        }
    }
    //从后向前遍历,更新子树中次数和
    //操作方法为先将sum数组复制到sum1中,对sum1进行“后缀和”
    for(int i=cnt;i>=0;i--)sum1[que[i]]=sum[que[i]];
    for(int i=cnt;i>=0;i--)sum1[nxt[que[i]]]+=sum1[que[i]];
}

int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        scanf("%s",ch);
        add(ch);
    }
    bfs();
    for(int i=1;i<=n;i++){
        printf("%d\n",sum1[pos[i]]);
    }
    return 0;
}
  • 6
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值