AC自动机

AC自动机用途

给多个字符串t,再询问一个字符串s,问有多少个字符串t出现在询问的字符串s中。

前置技能

学AC自动机之前,先学会什么是字典树,什么是kmpkmp我写过一篇博客,就不讲了,就是next 数组保存一个最长匹配前缀。字典树就更简单了,每个节点从根节点开始,出现一个字符就在父亲节点上连上下一个节点,也不多说。有需要再写一篇博客。

AC自动机

对于这个玩意,都说是字典树上跑KMP,到也没错。朴素KMP是在一个串上面跑next,而AC自动机只是变成了在字典树节点上跑fail指针,每个fail指针保存是最长匹配后缀。推荐一名大佬博客,讲的挺不错的。
朴素的写法,每一个字符串ts做一次kmp算法或者 对所有的t字符串建一颗字典树,然后每个位置匹配一下,很显然 O ( n 2 ) O(n^2) O(n2)的复杂度,绝对超时。
仔细思考一下,字典树里面,每次匹配都要从下一个位置开始跑一次匹配,类比一下没有kmp的朴素字符串匹配。是不是有点相似。普通单个字符串匹配,是不是枚举每一个位置,然后做一次暴力匹配?然后kmp 做了什么,通过next找到最大匹配前缀。那么我们是否可以在字典树用fail保存最大匹配后缀呢?显然是可以的,不然AC自动机干啥.
举个栗子: 假设有三个t asa aaa aas, 匹配aasab
字典树建成这个样子,丑了点,别在意,重在思想。
[外链图片转存失败(img-6sev97uO-1565147043932)(https://i.loli.net/2019/08/07/SkKvaoYQ6N8ejEV.png)]
然后一开始吧aas匹配掉了没啥意见吧,然后继续匹配aasa,很显然这个时候没有这个字符串,那么肯定就要转移,
转移到哪去呢?最长匹配后缀啊。如图是不是这样。

然后匹配asa,再继续匹配b,很显然失配了啊,然后找最长匹配后缀,只有根节点了,然后根节点背后又没有b。所以啥都没了,匹配最长后缀为空串。
好了,现在差不多理解这个算法的思想,接下来就是一些细节,不知道我还有没有没有考虑到的,如果有请在评论区提问。
1.上诉所讲的例子很明显,aasas跳转,如果后继没有a这个节点怎么办?asa 改成asb,asa要匹配谁呢?这个在kmp算法里面也有这个问题,很显然,继续找下去就能解决问题,假设 再加一个tsbaasb适配,aas调转到as没有后继b,再跳转到匹配后缀s,有后继b就匹配sb,类推,如果还没有找到就继续想上找。ps: 这个地方可以优化,fail指针找到了最长匹配后缀,然后字典树的下一个节点可以直接跳到对应位置如例子 aas后继a可以直接跳到为asa这个节点,不过不优化也没啥影响,因为字符串长度是固定的,最多不会跳超过n次,不优化只是多了个常数[外链图片转存失败(img-L5SSvgps-1565147043937)(https://i.loli.net/2019/08/07/DCchA7SJ1bVlua2.png)]
2.多个匹配怎么办?没到一个节点把他的fail找下去记录个数,如图字典树匹配sa,的时候会找他的fail,找到a然后把a这个节点记录,同理找sab,不仅仅把自己匹配了,还要把b匹配掉
3.怎么记录个数,如果一个字符串多次出现怎么办,看代码,记录一下就行了。

这个AC自动机的思想还是要学好,后面回文自动机要用到这个玩意的思想。

namespace Aho_Corasick_Automaton {
int trie[maxn][27];  //字典树
int cntword[maxn];   //记录单词出现次数,可以开一个vector记录是第几个单词
int fail[maxn];     // 失败回溯
int cnt=0;          // 树的节点个数

void init(int x) {
    for(int i=0; i<26; i++) {
        trie[x][i]=0;
    }
}

void insertWords(char *s) {
    int ls=strlen(s);
    int root=0;
    for(int i=0; i<ls; i++) {
        int next=s[i]-'a';
        if(!trie[root][next]) {
            init(++cnt);
            trie[root][next]=cnt;
        }
        root=trie[root][next];
    }
    cntword[root]++;
}

void getFial() {
    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.size()) {
        int now=q.front();
        q.pop();

        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];
        }
    }

}

int query(char *s) {
    int ls=strlen(s);
    int now =0,ans=0;
    for(int i=0; i<ls; i++) {
        now=trie[now][s[i]-'a'];
        for(int j=now; j&&cntword[j]!=-1; j=fail[j]) {
            // 如果这种状态已经计算过了就不用继续找下去了
            ans+=cntword[j];//统计个数,可以在这进行各种操作
            cntword[j]=-1;
        }
    }
    return ans;
}
}


再贴一个大哥板子

queue<int>q;
struct Aho_Corasick_Automaton {
    int c[N][26],val[N],fail[N],cnt;
    void ins(char *s) {
        int len=strlen(s);
        int now=0;
        for(int i=0; i<len; i++) {
            int v=s[i]-'a';
            if(!c[now][v])
                c[now][v]=++cnt;
            now=c[now][v];
        }
        val[now]++;
    }
    void build() {
        for(int i=0; i<26; i++)
            if(c[0][i])
                fail[c[0][i]]=0,q.push(c[0][i]);
        while(!q.empty()) {
            int u=q.front();
            q.pop();
            for(int i=0; i<26; i++)
                if(c[u][i])
                    fail[c[u][i]]=c[fail[u]][i],q.push(c[u][i]);
                else
                    c[u][i]=c[fail[u]][i];
        }
    }
    int query(char *s) {
        int len=strlen(s);
        int now=0,ans=0;
        for(int i=0; i<len; i++) {
            now=c[now][s[i]-'a'];
            for(int t=now; t&&~val[t]; t=fail[t])
                ans+=val[t],val[t]=-1;
        }
        return ans;
    }
} AC;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值