zoukankan      html  css  js  c++  java
  • AC自动机 算法详解(图解)及模板

    AC自动机 算法详解(图解)及模板

    文章转载于 [ bestsort]( https://blog.csdn.net/bestsort/article/details/82947639)
    1. 要学AC自动机需要自备两个前置技能:KMP和trie树(其实个人感觉不会kmp也行,失配指针的概念并不难)
    2. 其中,kmp是用于一对一的字符串匹配,而trie虽然能用于多模式匹配,但是每次匹配失败都需要进行回溯,如果模式串很长的话会很浪费时间,所以AC自动机应运而生,如同Manacher一样,AC自动机利用某些操作阻止了模式串匹配阶段的回溯,将时间复杂度优化到了(O(n)) (n)为文本串长度

    1. 下面开始用图学习ac自动机吧(个人比较喜欢放图,能用一张图解决的绝不叨叨)
      首先给定模式串("ash","shex","bcd","sha")然后我们根据模式串建立如下trie树:

      然后我们再了解下一步:
      ac自动机,就是在tire树的基础上,增加一个fail指针,如果当前点匹配失败,则将指针转移到fail指针指向的地方,这样就不用回溯,而可以路匹配下去了.(当前模式串后缀和fail指针指向的模式串部分前缀相同,如abce和bcd,我们找到c发现下一个要找的不是e,就跳到bcd中的c处,看看此处的下一个字符(d)是不是应该找的那一个)

    一般,fail指针的构建都是用bfs实现的
    首先每个模式串的首字母肯定是指向根节点的(一个字母你瞎指什么指,指了也是头字母有什么用嘛)

    1. 现在第一层bfs遍历完了,开始第二层
      (根节点为第0层)第二层a的子节点为s,但是我们还是要从a-z遍历,如果不存在这个子节点我们就让他指向根节点(如下图红色的a)

    2. 当我们遍历到s的时候,由于存在s这个节点,我们就让他的fail指针指向他父亲节点(a)的fail指针指向的那个节点(根)的具有相同字母的子节点(第一层的s),也就是这样

    3. 按照相同规律构建第二层后,到了第三层的h点,还是按照上面的规则,我们找到h的父亲节点(s)fail指针指向的那个位置(第一层的s)然后指向它所指向的相同字母根->s->h的这个链的h节点,如下图

    完全构造好后的树 然后匹配就很简单了,这里以ashe为例 我们先用ash匹配,到h了发现:诶这里ash是一个完整的模式串,好的ans++,然后找下一个e,可是ash后面没字母了啊,我们就跳到hfail指针指向的那个h继续找,还是没有?再跳,结果当前的h指向的是根节点,又从根节点找,然而还是没有找到e,程序END

    过程如下图


    
    #include<stdio.h>
    #include<queue>
    #include<string.h>
    #include<stdlib.h>
    #include<algorithm>
    using namespace std;
    const int maxn = 1e6 + 7;
    char str[maxn];
    struct acFind
    {
        int trie [maxn][26];
        int Count[maxn];//记录该单词出现次数
        int fail[maxn];//失败时的回溯指针
        int cnt = 0;
        void init()
        {
            cnt = 0;
            memset(trie,0,sizeof(trie));
            memset(fail, 0, maxn * sizeof(fail[0]));
            memset(Count, 0, maxn * sizeof(Count[0]));
        }
        void insertWords(char str[])
        {
            int root = 0;
            int len  = strlen(str);
            for(int i = 0; i < len; i ++)
            {
                int now = str[i] - 'a';
                if(!trie[root][now]){
                    trie[root][now] = ++ cnt;
                }
                root = trie[root][now];
            }
            Count[root] ++; //当前节点单词数+1
        }
        void getFail()
        {
            queue<int>que;
            for(int i = 0; i < 26; i++) //将第二层所有出现了的字母扔进队列
            {
                if(trie[0][i]){
                    que.push(trie[0][i]);
                    fail[trie[0][i]] = 0;
                }
            }
            //fail[now]    ->当前节点now的失败指针指向的地方
    ////tire[now][i] -> 下一个字母为i+'a'的节点的下标为tire[now][i]
            while(!que.empty())
            {
                int now = que.front();
                que.pop();
                for(int i = 0; i < 26; i ++)
                {
      //如果有这个子节点为字母i+'a',则
    //让这个节点的失败指针指向(((他父亲节点)的失败指针所指向的那个节点)的下一个节点)
    //有点绕,为了方便理解特意加了括号
                    if(trie[now][i]){
                        fail[trie[now][i]] = trie[fail[now]][i];
                        que.push(trie[now][i]);
                    }
                    else{//否则就让当前节点的这个子节点指向当前节点fail指针的这个子节点,就是匹配失败的时候要到的地方,在查询的时候用到
    //
    //                  0
    //                 / 
    //                a   s
    //               /     
    //              s       h1
    //             /       / 
    ///            h       a   e
    //       比如查询asha的时候,查询到ha的时候在最左边失配,将trie[h][a] = trie[h1][a],
    //      这只会在查询到底的时候会用到,查询ha的时候就查询的h1->a
    //
    //
    //
                        trie[now][i] = trie[fail[now]][i];
                    }
                }
            }
        }
        int query(char str[])
        {
            int now = 0, ans = 0;
            int len = strlen(str);
            for(int i = 0; i < len; i++)
            {
                 now = trie[now][str[i] - 'a'];
                 for(int j = now; j && Count[j] != -1; j = fail[j])
                 {
                     ans += Count[j];
                     Count[j] = -1;
                 }
            }
            return ans;
        }
    }AC;
    int main()
    {
        int t, n;
        scanf("%d",&t);
        while(t--)
        {
    
            scanf("%d", &n);
            AC.init();
            for(int i = 0; i < n; i ++)
            {
                 scanf("%s",str);
                  AC.insertWords(str);
            }
            AC.getFail();
            scanf("%s",str);
            int ans = AC.query(str);
            printf("%d
    ", ans);
        }
        return 0;
    }
    
  • 相关阅读:
    !!“理都懂”为什么“然并卵”?
    启明创投甘剑平:破解独角兽公司的基因密码
    任正非:华为3年前应该快垮了
    C语言 · 8皇后问题改编
    C语言 · 数的划分
    C语言 · 8皇后问题
    C语言 · 数字黑洞
    C语言 · FBI树
    C语言 · 超级玛丽
    C语言 · 数的划分
  • 原文地址:https://www.cnblogs.com/yuanlinghao/p/11305820.html
Copyright © 2011-2022 走看看