首页 > 其他分享 >AC自动机!

AC自动机!

时间:2022-12-31 17:55:48浏览次数:53  
标签:AC 匹配 指向 字母 fail 自动机 节点 指针

AC自动机

AC自动机,顾名思义,就是一种把题目输入后,能够自动生成AC代码的机器

AC自动机的名字来源于贝尔实验室的研究人员 Alfred V. Aho 和 Margaret J.Corasick,常常应用于模式匹配中。

举例子:对于某个字母串wcadqcareal中,我们想知道care, real, apple, cat等子串一共出现了几次。AC自动机便可以实行效率较高的模式匹配。

构建Trie树

对于如上例子,我们先构建一棵Trie树,如下图:

这里我们注意到,除了根节点,每个节点都保存了一个字母,并且从根节点的子节点向下走到某个位置时,这串路径可以表示一个子串。

这里需要注意一点,在构建的过程中并不能认为走到叶节点才算一个子串。比如car和care, car是care的前缀,走到e结尾的话,子串car会被忽略。因此需要记录一个节点是不是一个单词的末尾。

构建Trie树的过程,便是从上往下找,如果字母子节点已经存在,则继续往下走,否则创建子节点。

用数组模拟链表,相关代码如下:

点击查看代码
int tr[N][26], idx, cnt[N]; //tr数组的第一维表示节点编号,26表示不同小写字母的下一个走向,idx类似于指针,cnt数组表示这个子节点总共出现的次数。
bool st[N];标志某个节点是不是单词结尾

void insert(char str[]){
    int p = 0;//默认根节点编号为0
    for(int i = 0;str[i];i ++){
        int t = str[i] - 'a';
        if(!tr[p][t])tr[p][t] = ++ idx;//如果对应子节点不存在则创建子节点
        p = tr[p][t];//走向子节点
        cnt[p] ++;
    }
    st[p] = true;
}

AC自动机的fail指针

接下来会提到i和j两个指针. 其中i是正在匹配的字母串中的第一个字母,j是正在匹配的字母串中的最后一个字母。

比如例子中wcadqcareal, 我们正在从第六个字母c开始往后匹配,匹配到了r,那么i指向字母c,j指向字母r.

那我们便开始进行匹配。匹配的过程是,如果j指向的字母在Trie树中存在对应的子节点,那么j节点便指向对应的子节点,不然匹配失败,没有子串,需要将i指针在字母串中的位置向前移动一位,并将j的指针重新指向i.

比如我们正在从第二个字母c开始匹配,那么i和j都指向c, 接下来j指针从根节点向下查找,发现Trie树中存在对应子节点,那么j指针指向树中对应节点,再从对应节点中查找下一个字母d, 发现不存在相对应的子节点,那么匹配失败了,需要将i指向下一个字母a, 并让指针j指向i对应的a重新匹配。

那对于这个双指针算法,我们在对例子进行匹配的起初一段长度内都认为是无法匹配的,直到走完了care, 出现了一个匹配。那么接下来,需要让j回溯到a. 接下来继续匹配,可以匹配到real.

可是我们注意到一个问题,明明care的后缀和real的前缀存在相同部分re,那我们能不能想想办法,让i能够直接跳到Trie中后缀与前缀最长相同的地方,从而不进行指针j的回溯呢?

这就是我们接下来要构造的fail指针。fail指针,顾名思义,就是在匹配失败时,让指针指向的地方。

我们在这里规定一个单词的后缀或者前缀长度必须小于整个单词的长度

不妨就拿care这个词来举例子。首先是第一个字母,第一个字母构造fail指针的意义不大,让其直接指向root节点。

接下来是a这个节点。我们注意到对于ca这个子串的后缀a, 在Trie树中存在的确存在部分前缀与它相同,是appple的部分前缀。

那可以让a的fail指针指向apple的节点a。

接下来是r的fail指针。我们注意到存在相同最长后缀r, 是real的部分前缀,那我们可以让care中r的fail指针的指向real的节点r.

接下来是e的fail指针,我们注意到存在相同最长后缀re, 是real的部分前缀,那我们可以让care中e的fail指针的指向real的节点e.

那么我们构造出来的fail指针(红色线条)是这样的:

那我们可以对Trie树做进一步的处理:如果某个根节点在当前节点中不存在,但是在相同的前缀部分出现了,那我们就把当前节点向那个子节点连一条边。如下所示(蓝色是部分新连的边):

这是通过这种代码实现的:

点击查看代码
for(int i = 0;i < 26;i ++){
        int &p = tr[t][i];
        if(!p) p = tr[fail[t]][i];
}

前方已经说明了,fail[t]对应节点的字母与t节点对应字母是相同的。如果p节点不存在,那么只需让这个节点p指向前缀相同一部分后的姐节点就好了。那么可能将原本不匹配的情况变为匹配的情况。

那如何构建fail指针?

我们假设前方的fail指针已经构造好了。由于AC自动机里的后缀与前缀的重合部分是最大重合部分,那我只需要让新一个节点的fail指针指向下一个它父节点fail指针下对应节点的子节点即可。

听起来有点绕。举个例子:我们假设care下的r的fail指针已经指向了real的r对应节点,那么只需让e的fail指针指向real中的e对应节点。而这real中r节点对应的确存在e对应节点,那么便让care中e的fail指针,指向r的fail指针指向的real的r节点下的子节点e即可。

即fail[p] = tr[fail[t]][i]

其中p是t的子节点,i是小写字母字母转换的数值。

如果某一个子串前缀只有r但是没有re, 那么怎么办?

由于全局变量初始化为0, 那么没有re的部分,对应的tr指针会指向0, fail指针也会跟着对应指向0, 即根节点,这就实现了没有相同的后缀与前缀时从根节点重新开始寻找。
所以我们刚才在care这个子串的建立时时少加了一条边(绿色):

由于子串的后缀小于整个单词,我们在进行初始化时,只要按照树的深度依次添加字母,便可以实现Trie树的更新。这里通过队列BFS实现。

点击查看代码
int q[N];//模拟队列

void build(){
    int hh = 0, tt = -1;
    for(int i = 0;i < 26;i ++)
        if(tr[0][i])
            q[++ tt] = tr[0][i];//把与根节点联通的子节点加入队列
            
    while(hh <= tt){
        int t = q[hh ++];
        for(int i = 0;i < 26;i ++){
            int &p = tr[t][i];
            if(!p) p = tr[fail[t]][i];//如果子节点不存在,就让子节点指向父节点fail指针指向节点的对应节点
            else{//如果子节点存在
                fail[p] = tr[fail[t]][i];//建立fail指针
                q[++ tt] = p;//将该节点加入队列中以更新其他点。
            }
        }
    }
}

这样Trie树就被重建了。在进行匹配时,便不需要j的回溯了,i会一直沿着待匹配串一直走下去,提高了效率。

总结

AC自动机的核心在于构造fail指针(虽然这里完全采取了数组模拟)。将fail指针构造出来后,便可以实现树的重建。在不同的题目中,fail指针也会有其他作用,这个就要看具体题目了。

不过我们学AC自动机的一个目的就是希望能像名字一样做题AC嘛

感悟

写博客中途一定要保存。这次博客写到一般电脑卡机了,博客没有保存。随着电脑崩了,我的心态也崩了。

参考网站

AcWing

标签:AC,匹配,指向,字母,fail,自动机,节点,指针
From: https://www.cnblogs.com/Joci-zhuoxue/p/17016869.html

相关文章

  • AcWing 1359. 洛谷P1457 城堡
    解题思路\(\qquad\)这道题目是需要维护各种连通块信息的,所以这里我们可以也用并查集维护。这题我们如果注意一点细节,也是可以让代码变得很简洁的:\(\qquad\quad1.\)这道......
  • 掌握webpack(一)一张图让你明白webpack中output的filename、path、publicPath与主流插件
    webpack的核心概念,放到2022年相信很多的小伙伴都已经非常清楚了。但是,对于webpack配置中的output.path、output.filename以及output.publicPath,还有很多小伙伴还不理解。本......
  • macOS 终端运行提示“Operation not permitted ”解决办法
    终端运行命令后提“示Operationnotpermitted”报错,这个问题一般只有在macOSVentura系统出现比较频繁,或者是从其他版本升级到Ventura的也需要注意这个小问题。解决办法打......
  • 解决Linux Cache/Buffer及Swap过大的问题
    目录问题描述什么是Cache/Buffer?什么是Swap?什么是Cache/Buffer什么是SwapCache/Buffer及Swap过大会导致哪些问题?解决方法手动清除Cache/Buffer调整swapBuffer/Cache与......
  • Illustrator 2022 for mac (AI 2022) v26.4.1中文版
    Illustrator 2022中文版是一款矢量图形软件,Adobeillustrator常被称为“AI”,可以制作适用于印刷、Web、视频和移动设备的徽标、图标、绘图、版式和插图的矢量图设计软件。......
  • ASUS VivoBook FL8700JP 电脑 Hackintosh 黑苹果efi引导文件
    硬件型号驱动情况主板ASUSVivoBookFL8700JP处理器i7-1065G7已驱动内存8GbDDR4已驱动硬盘SSDM21TBWD730SNWESTERNDIGITAL已驱动显卡IntellrisPlusGraphics7已驱......
  • 快速体验React开发基础入门指南
    前言大家好,我是CoderBin,本次整理了我学习react过程中的各部分的知识点,看完文本你将会学到jsx的基本使用使用脚手架创建项目如何在React当中定义组件React当中的事件绑......
  • pikachu--验证码绕过(on client)
    文章内容简述:对密码进行暴力破解---客户端验证码的绕过。客户端验证码的绕过,原理主要是因为验证码的校验机制是在客户端本地的,也就是可以在本地的源码中可以看到的,所以......
  • 音视频:JavaCV 视频切片(MPEG-TS)(HLS)
    需要进行简单的音视频编程,如果不是特别数据C/C++,那么JavaCV应该是比较好的选择,下面记录一下使用JavaCV视频切片(MPEG-TS)(HLS)的方法。注意:存放HLS切片的目录必须存在(不会自......
  • 音视频:JavaCV 视频转码(硬件加速)(GPU)
    需要进行简单的音视频编程,如果不是特别数据C/C++,那么JavaCV应该是比较好的选择,下面记录一下使用JavaCV分离AAC视频数据(转封装的方式,不需要转码)的方法。使用硬件加速编码......