AC自动机学习笔记

本文详细介绍了AC自动机(Aho-Corasick Algorithm)的基础知识,包括核心变量如trie、fail、ans、pos和ed的含义,以及建字典树、构造fail指针和匹配文本串的流程。AC自动机常用于字符串匹配,能够高效地处理多个模式串。文章通过示例展示了如何计算不同模式串的出现次数,并提供了相关代码实现。
摘要由CSDN通过智能技术生成

变量释义

int trie[200005][26];   //AC自动机的字典树
int fail[200005];   //失配指针
int ans[200005];    //每个串出现的次数
int pos[200005];    //每个串的结尾的序号
int ed[200005];     //该节点是否为结尾,以该点结尾的模式串的数量

trie数组:仍然是字典树中的那个节点,但是需要注意节点另外一个非常重要的性质,代表一个从根到该点的前缀子串。
fail数组:失配指针,指向的是当前点代表的前缀子串在所有模式串中的最长后缀,创建这个数组的原因也很明显,当这个模式串匹配失败的时候,我们总是想要转移到一个已经匹配到的字符最多的模式串上去。
显然fail指针指向的点的深度必然小于当前点的深度,因此对整棵字典树构造fail指针要使用bfs

核心流程

AC自动机分三个步骤:建字典树,构造fail指针,跑文本串。中间可能还会需要建fail树。
建字典树:略。
代码:

int trie_insert(char *s){   //建立字典树
    int len=strlen(s),now=0;
    for(int i=0;i<len;i++){
        if(!trie[now][s[i]-'a']){
            trie[now][s[i]-'a']=++cnt;
        }
        now=trie[now][s[i]-'a'];
    }
    ed[now]++;
    return now;
}

构造fail指针:每次对某个字母连失配指针的时候,看父亲的失配指针是否有对应这个字母的转移,如果没有,则再往上找失配指针是否有对应这个字母的转移,直到找到或者到根节点。
但是有个问题:暴力跳fail指针可能会导致复杂度退化为 n 2 n^2 n2
解决方式:构造fail指针的同时记录下个能跳到的fail指针。即判断当前是否有字符i的转移,有则直接对fail赋值为父亲fail的转移,没有则将字符i的转移记录为父亲fail的转移,(有点并查集的味道)。
可以发现,失配指针构成了一棵树,且树上点的父亲一定是这个点对应的最长后缀。这个点的所有祖先都是这个点对应的最长后缀,且随着深度的减少,后缀的长度也下降。
所以,有个神奇的思想,将所有的模式串翻转后构造字典树,就是原来的失配指针树???不对。(事实上,构造出这样的例子也极少)
代码:

void get_fail() {   //获取fail标签
    for (int i=0;i<26;i++)
        if (trie[0][i]) q.push(trie[0][i]);
    while (!q.empty()) {
        int x=q.front(); q.pop();
        for (int i=0;i<26;i++)
            if (trie[x][i]) {	//fail树的路径压缩
                fail[trie[x][i]]=trie[fail[x]][i];
                q.push(trie[x][i]);
            }
            else trie[x][i]=trie[fail[x]][i];
    }
}

像上面这么写最大的好处就是在匹配的过程中完全用不到fail指针,减少了思维量和出错的概率。
跑文本串:就跟字典树一样跑就行了。。。因为之前的路径压缩的操作,不用再判断是否失配了。
代码:

void query(char *s){
    int len=strlen(s),now=0;
    for(int i=0;i<len;i++){
        now=trie[now][s[i]-'a'];
    }
}

时间复杂度 O ( n ∗ T ) O(n*T) O(nT), T T T为字符集的个数,一般为常数

典型例题

1.求共有多少个不同的模式串出现

因为一个点的fail树上的祖先全部为当前点对应的前缀子串的后缀,所以跑匹配的时候,跑到一个点,就要把这个点fail树上的所有祖先的出现次数加上ed的值,为了防止暴力跳fail指针导致的复杂度退化,又因为只需求是否出现,搜索到一个点后,就要把这个点打上标记,跳fail的时候遇到有标记的点就停止。
代码:

void query(char *s){
    int len=strlen(s),now=0;
    for(int i=0;i<len;i++){
        now=trie[now][s[i]-'a'];
        for(int j=now;j&&ed[j]!=-1;j=fail[j]){
            ans[j]+=ed[j];
            ed[j]=-1;
        }
    }
}

2.求模式串的出现次数

这时,打标记就没用了,考虑每次跳fail指针的时候都是从子孙跳到祖先,不妨统计每个祖先共被自己的子孙跳到过多少次,这时就需要建fail树了,统计子树的ans和乘上该点的ed值,即为祖先出现的次数,相应的,query函数中只需将ans[now]加ed[now]即可。
代码:

void query(char *s){    //查询模式串在文本串中出现的次数
    int len=strlen(s),now=0;
    for(int i=0;i<len;i++){
        now=trie[now][s[i]-'a'];
        ans[now]+=ed[now];
    }
}

建树:

for(int i=1;i<=cnt;i++){        //跳fail指针的优化,建立fail树,每个子串出现的次数转化为子树中的ans和。
        v[fail[i]].push_back(i);
    }

值得一提的是,很多字符串相关的算法中的指针树都是非常实用的,都需要深刻地理解。
比如:kmp中的前缀函数next树,ac自动机中的fail树,后缀自动机中的后缀链接link树。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值