AC自动机(Aho-Corasick Automaton),虽然不能够帮你自动AC,但是真的还是非常神奇的一个数据结构。AC自动机用来处理多模式串匹配问题,可以看做是KMP(单模式串匹配问题)的升级版。常常见到这样的说法,AC自动机 = Trie树 + KMP。

原理初步

首先对于所有的模式串,我们先需要利用Trie树将其建起来。AC自动机最巧妙的部分在于失配指针(fail)的构建,也就类似KMP中的next数组,只不过现在变为了多模式串。在匹配的时候沿着trie树走,发现不匹配即跳转失配指针,因此复杂度已经达到了理论下界$O(n + L*m) (其中L表示模式串数量)$

构建Trie树

最基本的Trie树的建立。其中End表示单词结尾

inline void Insert(char* s){
    , c;
    ; i < len; ++i){
        c = s[i] - 'a';
        if(!ch[u][c]) ch[u][c] = ++num_node;
        u = ch[u][c];
    }
    End[u]++;
}

构造失配指针

何为失配指针?对于任意一个节点$u$,$fail[u]$表示当节点$u$失配时它所应当到达的最优位置。暴力的做法中,当一个节点失配,我们即要从本次的开头往后移一格为开头重新匹配。就像KMP一样,我们的优化也就在于是不是只移一格?移动之后是否还从头开始匹配?

容易发现,如果我们移动一格以后没有东西与它匹配,那么以它作为开头毫无意义。而我们又不想浪费我们的历史信息。可以这样考虑——当我们确定一个新的开头时,为了考虑完全所有情况,我们要在理想情况下使得符合要求的开头的这一段长度尽量长。——当有一个部分失配的时候,意味着前面部分全部匹配。因此我们要考虑跳转一个位置,这个位置是当前已经匹配的这个串的最长公共前缀。因此我们又可以知道fail的另一个意义:指向当前节点所对应的串的最长公共前缀字符串的结尾节点。

那么具体怎么找呢?联系到KMP算法,KMP算法求解next数组是一个不断迭代的过程,如果$s[i]≠s[k]$,则不停做$k=next[k]$……直到找到$s[i]=s[k]$才能够转移。那么AC自动机也一样,不停做$u=fail[u]$,直到这两个节点相等。此时构建fail指针即可。事实上,它的原理和KMP一模一样

然而这里并不是一个模式串,我们要以什么顺序去遍历构造fail指针呢?既然想构造一个节点的fail需要访问诸多别的模式串的父亲节点,并且它访问的节点的深度不可能比自己还大(因为是前缀),所以采用BFS顺序来遍历即可

进行匹配

  构造好了fail指针,现在要正式进行匹配了。其实还是和KMP一样。逐位比较,不停fail直到当前位匹配为止,然后按照文本串的下一位继续走下去,如果不行继续fail。如果到达某一个单词的结尾了,意味着找到了一个匹配。

  但是有一个问题,如果有两个模式串$\{abcd\}\{bcd\}$时,当找到$\{abcd\}$匹配时,\{bcd\}肯定能够匹配。然而Trie树是前缀树,这两个模式串将会被分开建。那怎么办?因此找到一个串匹配时,还应当往回找所有该单词的后缀(Suffix)。一般的做法就是沿着fail不停往上走即可。(fail指针的另一个意义)

到目前为止,AC自动机就可以告一段落了。

Trie图

对AC自动机进行一些改进,原来的Trie树就变成了Trie图。事实上在实践中,人们常常会使用的是Trie图,它的效率更高,实现也更简单。Trie图的思想是可以补全Trie树中那些不存在的节点。

当一个真实存在的节点ch[u][i]在构建fail时,不再需要使用while语句,而是直接将fail指针直接连接到其父亲fail指针的相应儿子上,即使那个相应儿子根本不存在。

fail[ch[u][i]] = ch[fail[u]][i]

而对于一个节点所有不存在的儿子,把这些儿子直接作为失配的一个传送门,传送到应当到达的地点。

ch[u][i] = ch[fail[u]][i]

为什么这样做会是正确的呢?

想一想,何为失配?对于Trie树来说,失配就是预期到达的那个子节点不存在。因此我们通过赋值,令一个不存在的节点实际上是一个传送门。对于一个不存在的子节点ch[u][i],我们直接将它传送到其父亲的失配指针的相应节点——如果该相应节点存在那么最好;如果不存在,那么我们将会传送到一个传送门那里,因此继续被传送。注意这里的传送是连续的,因此其实它所指向的是另一个点,而那个点有可能指向另一个点,所以实际上指的有可能是一个很遥远的点。所以实际如果一直不存在,走一步也许就会被一直传送直到那个点存在。如果一路不存在,那也就指回了根节点。

那么对于一个存在的节点,我们也类似的,很草率的直接把它的fail设置成为它父亲相应的子节点,如果不存在,那么一样,是个传送门。特别注意,一个不存在的节点是没有fail的,因为它本身就是一个fail。

事实上我认为,如果仅仅只是为了判断是否匹配,那么根本不需要让真实存在的点有$fail$指针。恰恰,真实存在的节点的$fail$在这里存在的意义是为了统计匹配。当一个节点匹配一个模式串时,其所有后缀必定匹配。而此时我们需要通过这一步$fail$往上走。

Trie图实现的getFail:

inline void getFail(){
    ;
    ; i < ; ++i){
        ][i]){
            fail[ch[][i]] = ;
            q.push(ch[][i]);//这些单词的起始点的fail直接指向根节点
        }
    }
    while(!q.empty()){
        u = q.front(); q.pop();
        ; i < ; ++i){
            if(ch[u][i]){
                fail[ch[u][i]] = ch[fail[u]][i];//该节点存在,fail直接指向父节点的相应节点。
                q.push(ch[u][i]);//BFS,节点存在就需要入队。注意叶子结点也是会入队的
            }
            else ch[u][i] = ch[fail[u]][i];//如果不存在,就作为一个fail指针指向那个相应节点
        }
    }
} 

那么用Trie图进行匹配就更简单了。事实上,如果不是要统计所有的后缀链接,都不需要调用fail指针,因为失配的情况可以一视同仁

inline int AC_automaton(char* s){
    int len = strlen(s);
    ,v,ans=;
    ; i < len; ++i){
        u = ch[u][s[i]-'a'];//一视同仁地走到下一步
        ; v = fail[v]){
            if(!End[v]) break;
            ans += End[v], End[v]=;
        }
    }
    return ans;
}

「AC自动机」学习笔记的更多相关文章

  1. [AC自动机]【学习笔记】

    Keywords Search Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 131072/131072 K (Java/Others)To ...

  2. Note -「Lagrange 插值」学习笔记

    目录 问题引入 思考 Lagrange 插值法 插值过程 代码实现 实际应用 「洛谷 P4781」「模板」拉格朗日插值 「洛谷 P4463」calc 题意简述 数据规模 Solution Step 1 ...

  3. Note -「动态 DP」学习笔记

    目录 「CF 750E」New Year and Old Subsequence 「洛谷 P4719」「模板」"动态 DP" & 动态树分治 「洛谷 P6021」洪水 「S ...

  4. Note -「单位根反演」学习笔记

    \(\mathcal{Preface}\)   单位根反演,顾名思义就是用单位根变换一类式子的形式.有关单位根的基本概念可见我的这篇博客. \(\mathcal{Formula}\)   单位根反演的 ...

  5. 「Manacher算法」学习笔记

    觉得这篇文章写得特别劲,插图非常便于理解. 目的:求字符串中的最长回文子串. 算法思想 考虑维护一个数组$r[i]$代表回文半径.回文半径的定义为:对于一个以$i$为回文中心的奇数回文子串,设其为闭区 ...

  6. 「FHQ Treap」学习笔记

    话说天下大事,就像fhq treap —— 分久必合,合久必分 简单讲一讲.非旋treap主要依靠分裂和合并来实现操作.(递归,不维护fa不维护cnt) 合并的前提是两棵树的权值满足一边的最大的比另一 ...

  7. 「线性基」学习笔记and乱口胡总结

    还以为是什么非常高大上的东西花了1h不到就学好了 线性基 线性基可以在\(O(nlogx)\)的时间内计算出\(n\)个数的最大异或和(不需要相邻). 上述中\(x\)表示的最大的数. 如何实现 定义 ...

  8. 「Link-Cut Tree」学习笔记

    Link-Cut Tree,用来解决动态树问题. 宏观上,LCT维护的是森林而非树.因此存在多颗LCT.有点像动态的树剖(链的确定通过$Access$操作),每条链用一颗$splay$维护.$spla ...

  9. 「BZOJ 2434」「NOI 2011」阿狸的打字机「AC自动机」

    题意 有一个打字机,支持三种操作: 字符串末尾加一个小写字母 字符串末尾减一个字符 输出这个字符串 经过不超过\(n\)次操作后有\(m\)组询问:\((x,y)\),表示第\(x\)次输出第字符串在 ...

随机推荐

  1. PV、UV、IP、TPS、QPS、RPS、两天、吞吐量、 并发用户数 术语

    跟网站打交道,经常可以听到数据分析之类的专有名词,如pv多少.ip多少.tps多少之类的问题.下面就这些常见的数据给出其释义. PV   即 page view,页面浏览量         用户每一次 ...

  2. 避免使用HttpClient的系统代理

    这两天在玩Consul, 他的.Net驱动使用了HttpClient来发送Http请求. 但是我的电脑上装有SS, 所以请求会被SS过滤一次, 然后导致请求的延迟一直比较高. 然后只需要改写一下Htt ...

  3. Django组件 之 ookie 和 session

    -----------------------------------------------------------------------------------------相信自己,水滴石穿,不 ...

  4. case when then的用法-leetcode交换工资

    case具有两种格式:简单case函数和case搜索函数. --简单case函数 case sex when ' then '男' when ' then '女’ else '其他' end --ca ...

  5. IOS 开发之-- textfield和textview,return键的改变,点击return键

    IOS 开发之-- textfield和textview,return键的改变,点击return键 一,textfield的return键改变 方案1.改变键盘右下角的换行(enter)键为完成键,后 ...

  6. iOS基于B站的IJKPlayer框架的流媒体探究

    阅读数:6555 学习交流及技术讨论可新浪微博关注:极客James 一.流媒体 流媒体技术从传输形式上可以分为:渐进式下载和实施流媒体. 1.渐进式下载 它是介于实时播放和本地播放之间的一种播放方式, ...

  7. 帮助小白,最新版JDK的安装与环境变量配置(Win 10系统)

    学习JAVA,必须首先安装一下JDK(java development kit java开发工具包),之后再配置环境变量就可以开始使用JAVA了. 一,安装JDK 1,可以选择到官网下载最新版本的JD ...

  8. C++实现算法常用的STL---整理

    algorithm min(a,b)和max(a,b) #include<iostream> #include<algorithm> using namespace std; ...

  9. 解决只能root权限登陆mysql的问题

    一.问题描述 在用sqoop连接mysql时,会报错如下图所示,原因是mysql在默认情况下是使用了auth_socket plugin进行认证,即每次登陆都需要sudo mysql -u local ...

  10. rest framwork 小试身手

    models.py from django.db import models class Course(models.Model): """ 课程表 "&quo ...