AC自动机

参考博客https://www.cnblogs.com/sclbgw7/p/9260756.html

目录

一、例题

二、联系KMP思考AC自动机的构建

三、AC自动机匹配时跳fail边的优化

四、AC自动机对于单词节点计数时跳fail边的优化



一、例题

给出n个模式串,一个匹配串,询问有几个模式串出现在匹配串中


二、联系KMP思考AC自动机的构建

对于上面的问题我们如何用KMP算法做?

每个模式串构建一下失配数组,然后匹配模式串,整体复杂度为O(n*(l+L))

由于匹配串长度L可能很大,所以KMP的复杂度还是不太够,所以我们想能不能只让匹配串只跑一遍就对比完所有的模式串呢。

在哪跑?字典树上!所以先把模式串建到字典树上吧。

好的,现在你的脑中已经有一棵包含所有模式串的字典树了...

 

让我们再想想KMP失配数组构造的核心思想是什么?

当前节点失配就回到已匹配部分的最长相同前后缀位置进行匹配(深蓝色)

原本我们需要匹配串本身指针回到原本起点的下一位,同时模式串回到第一位开始比较,但是由于fail的定义,我们知道了已知匹配部分中匹配串能做起点的最优位置,就是最长相同前后缀  后缀的起点

这个起点往前作为起点 匹配不可能到达 最优起点(注定在最优起点前),否则当前就不是最长相同前后缀

这个点也不能再往后了,因为你可能错过能匹配完的情况

(题外话:同时我们如果要求的是模式串和匹配串的最长相同子串,跳fail边也不会影响,因为错过的最长匹配都不可能比当前失配状态已匹配部分长,因此跳fail只是加快了你求最长的脚步而忽略了比当前还小的)

然后我们并不需要从这个起点开始匹配,因为两者这一部分相同,所以都跳过这部分即可,匹配串指针相当于不动,模式串指针到失配位置的最长公共前后缀  前缀的后一位

将KMP的思想简单来说:已知匹配部分最长相同前后缀,当前节点失配,让匹配串不动,模式串从最长相同前后缀后一位开始与匹配串当前节点匹配。

那么AC自动机呢?

暴力的方法就是匹配串从字典树根节点开始直到失配,然后匹配串下一位作为起点再从根节点出发...

对于字典树来说我们每次匹配肯定是沿着某一条树链一直匹配下去直到失配,我们把这条树链当成模式串,此时我们肯定也是利用最长相同前后缀让匹配串找到最优起字典树字典树,让这个起点之前的起点一定没有超过当前匹配量因而不会漏掉什么没有统计的。然后字典树根节点和匹配串同时从起点下移最长相同前后缀,使得匹配串不回溯,字典树直接从最长相同前缀后一位开始匹配。

那么具体应该怎么实现呢?

首先你要让根节点的所有子节点指向自己,为了不特判。

然后你要开始BFS一层层将求当前的fail,我们每次都利用父节点的fail,然后跳到那个位置看那个位置子节点是否有当前的字母,

没有的话再跳fail边跳到有,跳到最后就跳到了根节点,根节点所有儿子我们都指向了根节点,那么fail[now]就指向那个节点了,这就是之前全部指向根节点的作用。(过程跟KMP基本一样)

需要BFS遍历求的原因是为了保证求解当前节点所可能涉及到的节点的fail都已经正确求出。

代码:(白嫖自参考博客)

void build()
{
    for(int i=0;i<26;++i)ch[0][i]=1;
    fail[1]=0;
    queue<int>q;
    q.push(1);
    while(!q.empty())
    {
        int x=q.front();q.pop();
        for(int i=0;i<26;++i)
        {
            int c=ch[x][i];
            if(!c)continue;
            int fa=fail[x];
            while(fa&&!ch[fa][i])fa=fail[fa];
            fail[c]=ch[fa][i];
            q.push(c);
        }
    }
}

然后我们思考个问题:怎么计数?

当前匹配过程中由于没有失配,等会失配位置再跳会在已知匹配部分中漏掉很多完整单词。

所以每遇到一个节点,我们首先对这个单词计数,可以跳fail边,这样就不会错过所有后缀的可能成为单词的情况,只要我们每个点都跳fail跳完就会把所有已匹配部分的后缀作为完整单词的情况记录下来。

对此的优化:另开一个bool数组end[]记录当前fail链上(包括自己)是否有单词末尾节点,有的话我们才跳这个点统计,很显然,当前的fail[now]的end[]求出的是之前整条fail链上最近出现单词的节点,当前end[now]的求解只需根据end[fail[now]]即可

if (end[fail[now]] || val[now]) end[now] = true;///val[now]表示当前节点是结尾节点

三、AC自动机匹配时跳fail边的优化

我们知道目前构造的AC自动机与匹配串匹配时当前位置失配时我们会跳fail边,到达已经匹配部分最长相同前后缀部分,看哪里有没有当前需要的子节点,直到找到。

这样跳来跳去导致复杂度退化回KMP的那个复杂度。

那么我们要怎么做呢?

当然是希望只跳一次fail,然后让fail[now]告诉你它这个子节点自己跳到哪得到的。

所以我们对当前节点所有不存在的以及存在的子节点让他们指向指向失配节点的对应子节点,我们知道由于BFS,fail点的所有信息都已经求出来了,于是fail点就告诉了你他是跑到哪儿找到的,每次我们都只要听上一级fail的信息就行,我们直接让当前的节点指向fail节点对应位置所说的节点,于是整个字典树就连成了一个图。所以这么一来我们根本就不存在什么失配情况了,因为失配的节点全部都指向了跳好fail边匹配后的节点,所以整个匹配过程就用不到fail,直接在这和Trie图上跑就行了。


四、AC自动机对于单词节点计数时跳fail边的优化

这个时候也可以将计数的优化了,之前的计数虽然优化了一下,只有fail链有单词末尾节点,才去暴力跳那个fail链,现在我们要用上面的思想,搞一个last数组,表示当前节点跳fail链途中,上一个单词末尾节点,也是利用好fail链信息的传递,直接调用

last[fail[now]]即可知道last[now]

板子:

const int maxnode = 1e6+5;//模式串数量*长度
const int ALP = 26;//字符种类数

struct AC_am
{
    queue<int>que;
    int sz;
    int trie[maxnode][ALP];
    int fail[maxnode];
    int last[maxnode];
    int val[maxnode];//储存当前节点信息,如是否为单词节点等等

    int newnode(int x){
        memset(trie[x],0,sizeof trie[x]);
        val[x] = 0;
        return sz++;
    }

    void init(){
        newnode(sz = 0);
    }

    int idx(char ch){//实际字符串转化为字典树对应节点,根据题目做出具体改变
        return ch-'a';
    }

    void insert(char *s){
        int u = 0;
        for (int i=0;s[i];i++){
            int c = idx(s[i]);
            if (!trie[u][c]){
                trie[u][c] = newnode(sz);
            }
            u = trie[u][c];
        }
        val[u]++;
    }

    void build(){
        fail[0] = 0;
        for (int c=0;c<ALP;c++){
            int v = trie[0][c];
            if (v){
                que.push(v);
                fail[v] = 0;
                last[v] = 0;
            }
        }
        while (!que.empty()){
            int u = que.front();que.pop();
            for (int c=0;c<ALP;c++){
                int v = trie[u][c];
                if (!v){
                    trie[u][c] = trie[fail[u]][c];
                    continue;
                }
                int fa = fail[u];
                //while (fa && !trie[fa][c]) fa = fail[fa];///这行去掉,会快很多
                fail[v] = trie[fa][c];
                last[v] = val[fail[v]]? fail[v]:last[fail[v]];
                que.push(v);
            }
        }
    }

    int count(int x){
        int cnt = 0;
        while(x){
            cnt += val[x];
            val[x] = 0;
            x = last[x];
        }
        return cnt;
    }

    int find(char *s){
        int ans = 0;
        int u = 0;
        for (int i=0;s[i];i++){
            int c = idx(s[i]);
            u = trie[u][c];
            if (val[u]) ans += count(u);
            else if (last[u]) ans += count(last[u]);
            //printf("nowans=%d\n",ans);
        }
        return ans;
    }

}ac;

​

这题的代码:

#include<cstdio>
#include<cstring>
#include<queue>
 
using namespace std;
 
const int maxnode = 1e6+5;
const int sigma_size = 26;
 
int trie[maxnode][sigma_size],sz;
int val[maxnode];
int last[maxnode],fail[maxnode];
queue<int>que;
char s[maxnode];
 
void init(int x)
{
    val[x] = 0;
    memset(trie[x],0,sizeof trie[x]);
}
 
int idx(char s){return s-'a';}
 
void insert(char *s)
{
    int u = 0;
    for (int i=0;s[i];i++){
        int v = idx(s[i]);
        if (!trie[u][v]){
            init(sz);
            trie[u][v] = sz++;
        }
        u = trie[u][v];
    }
    val[u]++;
}
 
void getfail()
{
    fail[0] = 0;
    for (int c=0;c<sigma_size;c++)///与上面白嫖代码一个意思,反正就是让根节点所有自节点(不管存不存在)fail=根节点
        if (trie[0][c]){
            int u = trie[0][c];
            que.push(u);
            fail[u] = last[u] = 0;
        }
    while (!que.empty()){
        int r = que.front(); que.pop();
        for (int c=0;c<sigma_size;c++){
            int u = trie[r][c];
            if (!u){///不存在的子节点由fail已求信息,一次跳到符合的点,建立Tire图,用于匹配的
                trie[r][c] = trie[fail[r]][c];
                continue;
            }
            que.push(u);
            int v = fail[r];
            fail[u] = trie[v][c];
            last[u] = val[fail[u]]? fail[u]:last[fail[u]];
        }
    }
}
 
int cal(int j)
{
    int ans = 0;
    while (j){
        ans += val[j];
        val[j] = 0;///不加这句话这个串会被重复计算,那就是求数量,而不是种类了
        j = last[j];
    }
    return ans;
}
 
int Find(char *s)
{
    int j=0,ans=0;
    for (int i=0;s[i];i++){
        int c = idx(s[i]);
        j = trie[j][c];
        if (val[j]) ans += cal(j);///val表示是单词结尾
        else if (last[j]) ans += cal(last[j]);///last链上有单词结尾
    }
    return ans;
}
 
int main()
{
    int n;
    sz = 1;///字典树新开辟节点的编号
    scanf("%d",&n);
    while (n--) scanf("%s",s),insert(s);
    getfail();
    
    scanf("%s",s);
    printf("%d\n",Find(s));
    return 0;
}

总结:比几个月前第一次写应该清楚多了,不过本着通过写博客加深理解的心态写的,很多地方很粗的想想是挺对的,但是复杂度的分析真的不是很清楚,既然别人说O(n*l)构造那就这样吧。有什么错误刷刷题回来再改。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值