AC自动机——学习笔记

什么是自动机

是一种数学模型,大概就是由一堆状态和状态转移规则等东西构成,能与外界交换信息,并改变动作。
这个是理论上的东西,了解就行,对AC自动机的理解没有大影响。

什么是AC自动机?

通俗的讲就是在Trie上做kmp,处理多模式串匹配问题。
Trie 的每个结点就是一个状态,根结点是初始状态。
AC自动机的行为被定义为一下3个函数:
1. next函数 ch(q,a):返回从当前状态q走值为a的边后所到达的状态。如果根节点没有值为a的边,那么ch(0,a)=0,(就是说:当读入不匹配的字符时,自动机保持在初始状态)
如果从结点q出发有一条边值为a到达结点v,那么 ch(q,a)=v,否则ch(q,a)=Φ。
2. fail函数 fail(q):返回从状态q(q!=root)匹配错误时要转移到的状态
记L(q)表示Trie上q节点对于的字符串,
如果L(v)是L(q)的一个后缀,且是最长的后缀,则fail(q)=v(kmp的思想)。
fail(q)总是能返回一个状态,因为L(root)=Φ是任何模式串的前缀
3. output函数out(q):输出在状态q时,所有匹配的模式串。

上面的描述比较形式化。简单来说:
先把所有待匹配串插入到一颗Trie中。
对于每个节点,我们需要额外计算一些信息:
fail指针:如上所述,类似kmp中的next表。
cnt : 若这个位置是某个模式串的结尾,就cnt++。
last指针:指向沿着当前节点的fail指针往回走,第一个遇到的cnt>0的节点。是用于output的。

匹配

假设我们已经构造出了这些信息。考虑如何进行匹配,大概流程是这样的:
根据目标串一个一个字符在Trie上走,
每走到一个节点,执行一次output(顺着last指针往后累加答案)。走到下个节点时,如果ch(p,s[i])不为空则直接走过去,否则沿着fail指针走,直到匹配成功或者到初始状态(还是kmp)
注意到“沿着fail指针走,直到匹配成功或者到初始状态”这个过程较繁琐,而且只有ch(p,s[i])为空时才会执行,就想到可以优化一下,直接把ch(p,s[i])指到要跳到的节点。
代码如下:

int Find(P_node p){  // 即output
    int res=0;
    for(;p!=root;p=p->last) res+=p->cnt, p->cnt=0; 
    return res;  
}  

int Query(char* s){  // 匹配
    int res=0, len=strlen(s);
    P_node p=root;
    for(int i=0;i<=len-1;i++) p=p->ch[s[i]-'a'], res+=Find(p);
    return res;
}

构造

回到如何构造这些信息,显然都可以一次对Trie的bfs遍历实现,具体对于p的某个儿子v:

v->fail=p->fail->ch[i];
v->last=(v->fail->cnt?v->fail:v->fail->last);

要注意的是root的直接儿子的fail和last都指向root。
理解定义后还是比较显然的。

模板题PDU2222代码:

#include<cstdio>
#include<cstring>
#include<queue>
#include<algorithm>
using namespace std;
struct node{
    int cnt; node *fail,*last; 
    node* ch[27];
} base[500000], *len, nil, *null=&nil, *root;
typedef node* P_node;
P_node newnode(node* son=NULL){
    len->cnt=0; len->fail=son; for(int i=0;i<=25;i++) len->ch[i]=son; 
    return len++;
}
void Insert(P_node &p,char *now){
    if(p==null) p=newnode(null);
    if((*now)=='\000'){ p->cnt++; return; }
    Insert(p->ch[(*now)-'a'],now+1);
}
queue<P_node> que;
void build(){
    root->fail=root->last=root;
    for(int i=0;i<=25;i++) if(root->ch[i]!=null){
        P_node v=root->ch[i]; que.push(v);
        v->fail=v->last=root;
    } else root->ch[i]=root;
    while(!que.empty()){
        P_node p=que.front(); que.pop();
        for(int i=0;i<=25;i++) if(p->ch[i]!=null){
            P_node v=p->ch[i]; que.push(v);
            v->fail=p->fail->ch[i];
            v->last=(v->fail->cnt?v->fail:v->fail->last);  
        } else p->ch[i]=p->fail->ch[i];
    }
}
int Find(P_node p){
    int res=0;
    for(;p!=root;p=p->last) res+=p->cnt, p->cnt=0; 
    return res;  
}  
int Query(char* s){ 
    int res=0, len=strlen(s);
    P_node p=root;
    for(int i=0;i<=len-1;i++) p=p->ch[s[i]-'a'], res+=Find(p);
    return res;
}
int _test,n,m;
char s[1000005];
int main(){
    freopen("ac.in","r",stdin);
    freopen("ac.out","w",stdout);
    scanf("%d",&_test);
    while(_test--){
        len=base; root=newnode(null); 
        scanf("%d",&n);
        for(int i=1;i<=n;i++) scanf("%s",s), Insert(root,s);
        build();    
        scanf("%s",s); printf("%d\n",Query(s)); 
    }
    return 0;
} 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值