AC自动机的实现思想与原理

news2024/11/24 17:24:48

1. 基本介绍

本文最先发布于博客园,原地址:AC自动机的实现与思想原理 - yelanyanyu - 博客园 (cnblogs.com)

1.1案例引入

有一个字典有若干的敏感词 String[] str;,有一个大文章 string,我们要找到大文章中出现的所有的敏感词,并得知其位置,收集到每一个敏感词。
上例就是 AC 自动机的经典案例。

1.2 约定

  • 入边与出边。假设有一个节点 x,那么进入这个节点的边就称为入边 ,出这个节点的就是出边

1.3 介绍

所谓 AC 自动机就是前缀树+KMP 算法。利用前缀树查询前缀高效的特点,配合 KMP 充分利用前后缀对称的思想,我们可以做到高效的多个模式字符的查询功能。
为了 AC 自动机,前缀树的节点结构需要做一些改变。所以我们先对前缀树进行相应的改造,然后再配合 KMP 算法思想进行讲解。

1.4 前缀树

[[1-10 前缀树]]
我们用这些敏感词去构建一颗前缀树。
节点结构如下:

public static class Node {  
    public String end;  
    public boolean endUse;  
    public Node fail;  
    public Node[] nexts;  
  
    public Node() {  
        endUse = false;  
        end = null;  
        fail = null;  
        nexts = new Node[26];  
    }  
}

1.4.1 fail 指针

给每一个节点加一个 fail 指针(按照宽度优先遍历的方式设置 fail 指针),构建流程

  • 头节点的 fail 指针指向空(人为规定);
  • 头节点的子节点(第一级节点)一律指向头部(人为规定);
  • 对于任意一级的节点 x,看其父亲的 fail 指针指向的节点y是否与 x 相连:
    • 不相连,就看 y 的 fail 指向的节点z是否与 x 相连,一直循环遍历,若直到头节点都没有与 x 相连就让 x 指向头;
    • 相连,就将 x 的 fail 指针指向 x;
      故从上可以看出 fail 指针有一个特点,就是一个节点的 fail 指针所指向的节点满足:其入边(进入该节点的边)的权值必定是相同的。

1.4.2 end 变量

我们利用 end 变量来标记每一个模式字符串的结束,并且在这个节点存储这整个模式字符串的值。

1.4.3 endUse 变量

在前缀树的节点中,我们有有一个 pass 变量用来记录,到达这个节点的次数,但是在 AC 自动机中,我们并不需要得知这个单词或前缀出现了几次,我们只需要知道模式字符串是否在待匹配文章中出现,为了避免重复匹配带来的效率浪费,所以我们设置一个变量 endUse 用来标记这个单词是否已经被匹配过了。

1.5 KMP 算法

[[KMP算法]]
具体见 3000+长文带你进入KMP算法思想 - yelanyanyu - 博客园 (cnblogs.com)。
让我们简单回顾一下,KMP 算法的思想。KMP 算法利用了模式字符串中前后缀最大对称子串来做到最大限度的利用上一次匹配的结果来方便于这一次的匹配,极大的提高了效率。
这将为 AC 自动机的构建思想提供根本上的指导。我们将在后面讲到。

1.6 时间复杂度

由于 AC 自动机利用了改进前缀树和 KMP 算法,所以时间复杂度可以达到 O ( N ) O(N) O(N) (N 为待匹配文章的长度)。

2. AC 自动机的实现流程

我们先说清楚流程,再讨论其精髓。
我们借助一颗前缀树来理解(如下图所示):
需要注意,我们省略了些人为规定的 fail 指针——第一行指向 root 节点的 fail 指针。
规定,我们说节点 x 就是指入边为 x 的节点。所有黑色的节点代表一个模式字符串的结尾节点。
我们给出一个待匹配的文章 content:abckabc。模式字符串 abc,bck,ck,fail 指针用带虚线的有向边表示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lrS5Tvqa-1671333989341)(null)]

2.1 变量指定

我们将利用四个变量 index,cur,follow,ans。
index 表示当前字符,即在树中,当前选择的路(边)。
cur 表示当前到达的节点。
follow 也是一个指针用来完整的走一圈 fail 指针,若碰到了黑色实心节点,就让其加入结果集 ans。
ans 表示结果集,原文 content 中已经匹配上的模式字符串。

2.2 过程描述

  1. 对于每一个节点 x,设当前需要走的路为 index,那么就看 x 是否有 index 的路可以走:
  • x 有 index 的路可以走。更新 cur 到下一个节点,同时 follow 指向当前的 cur(转到步骤 2);
  • x 没有 index 的路可以走,并且 x 不是 root。沿着 fail 指针更新 cur,直到有 index 的路可以走为止;若一直都没有路走,就会回到并且停在 root。
  • x 就为 root。判断有没有 index 路可以走,没有走就停在 root。
  1. 对于每一个节点 x,都有一个 follow 指针指向更新后的 cur。随后 follow 绕着当前节点的 fail 指针遍历,直到遇到 root 时停止行动。过程中,若碰到了黑色的节点就记录当前模式字符串到 ans 中,若遇到的黑色节点已经走过,就直接结束遍历。
    以下表格就是该实例下的演示。其中->表示各个指针的移动轨迹。

3. AC 自动机思想原理

想必大家一定有很多疑问:

  • 为什么要设置 fail 指针?fail 指针有什么用?
  • 前缀树是怎么与 KMP 算法进行结合的?
  • AC 自动机究竟是怎么做到高效的?
    我们将针对上述问题来阐述 AC 自动机的思想原理。

3.1 fail 指针的作用

由于其入边(进入该节点的边)的权值必定是相同的。所以,对于两颗前缀子树必定存在有一个字符相同。
若该节点 x x x 的父节点 f f f 恰好连上了另一颗子树的对应节点 x ′ x^{'} x 的父节点 f ′ f^{'} f,那么显而易见,两个前缀子树就有两个字符是相邻且相同的了。
并且这两个字符所在的模式串有一个特点,就是以 x x x 为结尾的后缀串( y x yx yx)恰好就是以 x ′ x^{'} x 开头的前缀串( x ′ y ′ x^{'}y^{'} xy)。这就是 fail 指针的核心作用,用于构造出这样的一对相等的前后缀。


当我们利用 fail 指针进行跳转的时候,就意味着那个节点所代表的模式字符串已经匹配失败了,所以我们应该用另一个模式字符串进行匹配。这是 fail 指针的作用其二

3.2 前缀树和 KMP 算法思想

我们利用 fail 构建出两个模式串之间相同的前后缀,之后 KMP 算法思想才算是真正的显露出来。我们利用 fail 指针跳转到 x ′ x^{'} x,这样就算是继承了前一个部分匹配的结果了。


现在我们来说明 AC 自动机是如何做到最大部分匹配的。我们知道只要其父亲 fail 指针指向的节点与当前节点相同,那么当前节点的 fail 指针就是指向那个相同的节点。那么对于两个模式字符串来说,其之间连续的相同的子串都会有 fail 指针相连。所以对于某一次匹配来说,这些相同的字符就不可能再多一个了,所以这就是最大的部分匹配。

3.3 实例说明

还是以这颗前缀树为例子。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vnQkXcEB-1671333989533)(null)]

解读:

  • 假设当我们走到了 c1 节点就走投无路了,那么根据流程我们就会走到 c2 节点。此时我们可以发现 a1b1c1 与 b2c2k2 这两颗前缀子树所代表的字符串 abc 与 bck,恰好分别有以 c 为结尾的后缀子串 bc 和前缀子串 bc,这两个子串是相同的。当我们根据 fail 指针走向 c2 时就意味着 abc 模式匹配的失败,我们继承了其后缀 bc 转而向含有相同子串 bc 的模式串 bck 尝试进行匹配——原有的后缀变成了前缀;
  • 若我们在 bck 模式仍然匹配失败,那么我们就会根据 fail 指针继承后缀 ck,尝试去匹配第三个模式 ck;
  • 若这三个模式都没有加入 ans 的行为,则说明没有在 content 中找到模式串。
  • 不过我们也需要明确,若 b1 的 fail 指针没有指向 b2(即 b2 节点不再是 b2 指向自身了),那么 c1 的 fail 指针就不会指向 c2,而是指向 c3 了。因为在这种情况下,b1 的 fail 指针只能指向 root 节点,c1 自然也只能找到 c3 了。所以,当 b1 不指向 b2 的时候,以 c 结尾的后缀串 bc 就不能转化成为 2 号子树的前缀串。这种情况进一步的说明了,利用 fail 指针进行前后缀匹配的正确性。

3.4 AC 自动机的高效本质

我们先利用前缀树方便且高效的找到了两个模式字符串的相似之处(以某个字符为结尾的前后缀相同),然后再利用 KMP 思想利用上一次匹配(上一次根据模式字符串 A 的匹配失败)的结果优化下一次匹配的过程。这就是 AC 自动机高效的本质。

4. 代码实现

/**  
 * 前缀树的节点  
 */  
public static class Node {  
    /**  
     * 如果一个node,end为空,不是结尾  
     * 如果end不为空,表示这个点是某个字符串的结尾,end的值就是这个字符串  
     */  
    public String end;  
    /**  
     * 只有在上面的end变量不为空的时候,endUse才有意义  
     * 表示,这个字符串之前有没有加入过答案  
     * 防止重复收集  
     */  
    public boolean endUse;  
    public Node fail;  
    public Node[] nexts;  
  
    public Node() {  
        endUse = false;  
        end = null;  
        fail = null;  
        nexts = new Node[26];  
    }  
}  
  
/**  
 * AC自动机  
 */  
public static class ACAutomation {  
    private Node root;  
  
    public ACAutomation() {  
        root = new Node();  
    }  
  
    /**  
     * 先把所有敏感词挂到前缀树上,但是不连fail指针  
     *  
     * @param s  
     */  
    public void insertNode(String s) {  
        char[] str = s.toCharArray();  
        Node cur = root;  
        int index = 0;  
        for (int i = 0; i < str.length; i++) {  
            index = str[i] - 'a';  
            //没有路就新建一个  
            if (cur.nexts[index] == null) {  
                Node next = new Node();  
                cur.nexts[index] = next;  
            }  
            cur = cur.nexts[index];  
        }  
        cur.end = s;  
    }  
  
    /**  
     * 构建fail指针  
     */  
    public void buildFail() {  
        //队列用于宽度优先遍历  
        Queue<Node> queue = new LinkedList<>();  
        queue.add(root);  
        Node cur = null;  
        Node cfail = null;  
        //任何一个父亲出来的时候,设置其子节点的fail指针  
        while (!queue.isEmpty()) {  
            /*  
            当前节点弹出,当前节点的所有后代加入到队列里去,  
            当前节点给它的子去设置fail指针  
             */            
            cur = queue.poll();  
            // 所有的路  
            for (int i = 0; i < 26; i++) {  
                /*  
                cur -> 父亲,i号儿子,必须把i号儿子的fail指针设置好  
                如果真的有i号儿子  
                 */                
                if (cur.nexts[i] != null) {  
                    cur.nexts[i].fail = root;  
                    cfail = cur.fail;  
                    //寻找i号儿子该连谁  
                    while (cfail != null) {  
                        if (cfail.nexts[i] != null) {  
                            cur.nexts[i].fail = cfail.nexts[i];  
                            break;  
                        }  
                        cfail = cfail.fail;  
                    }  
                    queue.add(cur.nexts[i]);  
                }  
            }  
        }  
    }  
  
    /**  
     * 查询是否有匹配的模式字符串  
     * @param content  
     * @return 所有匹配到的模式字符串  
     */  
    public List<String> containWords(String content) {  
        char[] str = content.toCharArray();  
        Node cur = root;  
        Node follow = null;  
        int index = 0;  
        List<String> ans = new ArrayList<>();  
        //对于每一个字符  
        for (int i = 0; i < str.length; i++) {  
            index = str[i] - 'a'; // 路  
            // 如果当前字符在这条路上没配出来,就随着fail方向走向下条路径  
            while (cur.nexts[index] == null && cur != root) {  
                cur = cur.fail;  
            }  
            /*  
            两种情况:  
             1) 现在来到的路径,是可以继续匹配的  
             2) 现在来到的节点,就是前缀树的根节点  
             判断是能继续往下走,还是走到了头节点,走投无路  
             */            
            cur = cur.nexts[index] != null ? cur.nexts[index] : root;  
            //抓住敏感词的变量  
            follow = cur;  
            //来到任何一个节点过一圈  
            while (follow != root) {  
                //这个敏感词已经记录过了  
                if (follow.endUse) {  
                    break;  
                }  
                // 不同的需求,在这一段之间修改  
                if (follow.end != null) {  
                    ans.add(follow.end);  
                    follow.endUse = true;  
                }  
                // 不同的需求,在这一段之间修改  
                follow = follow.fail;  
            }  
        }  
        return ans;  
    }  
  
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/99453.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

物联网ARM开发- 6协议 FSMC模拟8080时序驱动LCD(上)

目录 一、常见显示器介绍 1、显示器分类 2、显示器的基本参数 二、TFT-LCD控制原理 1、TFT-LCD结构 2、TFT-LCD控制框图 3、控制原理 LCD数据传输时序 LCD数据传输时序参数 三、SSD1963液晶控制器 1、SSD1963液晶控制器 2、SSD1963内部框图分析 3、8080写时序…

RK3568平台开发系列讲解(音视频篇)FFmpeg公共基础参数

🚀返回专栏总目录 文章目录 一、公共操作部分二、每个文件主要操作部分三、视频操作部分四、音频操作部分沉淀、分享、成长,让自己和他人都能有所收获!😄 📢当我们使用 FFmpeg 时,有一些贯穿 FFmpeg 各个组件的核心参数,在我们查看帮助信息时就可以看到,help 不带参…

基于 Tensorflow 2.x 实现多层卷积神经网络,实践 Fashion MNIST 服装图像识别

一、 Fashion MNIST 服装数据集 Fashion MNIST 数据集&#xff0c;该数据集包含 10 个类别的 70000 个灰度图像。大小统一是 28x28的长宽&#xff0c;其中 60000 张作为训练数据&#xff0c;10000张作为测试数据&#xff0c;该数据集已被封装在了 tf.keras.datasets 工具包下&…

move functions with VS without noexcept

本文所讲对移动函数使用noexcept修饰时带来的效率提升只针对std::vector。而对std::deque来说没有功效。 1. 针对std::vector 1.1 move functions with noexcept 当移动构造函数有noexcept修饰时&#xff0c;在对std::vector进行push_back扩充致使vector的size等于capacity时…

26. GPU以及 没有gpu的情况下使用colab

在PyTorch中&#xff0c;CPU和GPU可以用torch.device(‘cpu’) 和torch.device(‘cuda’)表示。 应该注意的是&#xff0c;cpu设备意味着所有物理CPU和内存&#xff0c; 这意味着PyTorch的计算将尝试使用所有CPU核心。 然而&#xff0c;gpu设备只代表一个卡和相应的显存。 如果…

【大数据技术Hadoop+Spark】Spark SQL、DataFrame、Dataset的讲解及操作演示(图文解释)

一、Spark SQL简介 park SQL是spark的一个模块&#xff0c;主要用于进行结构化数据的SQL查询引擎&#xff0c;开发人员能够通过使用SQL语句&#xff0c;实现对结构化数据的处理&#xff0c;开发人员可以不了解Scala语言和Spark常用API&#xff0c;通过spark SQL&#xff0c;可…

数据挖掘Java——Kmeans算法的实现

一、K-means算法的前置知识 k-means算法&#xff0c;也被称为k-平均或k-均值&#xff0c;是一种得到最广泛使用的聚类算法。相似度的计算根据一个簇中对象的平均值来进行。算法首先随机地选择k个对象&#xff0c;每个对象初始地代表了一个簇的平均值或中心。对剩余的每个对象根…

给 VitePress 添加 algolia 搜索

大家好&#xff0c;我是 Chocolate。 最近在折腾 VitePress&#xff0c;搭建了一个文档项目&#xff1a;ChoDocs&#xff0c;不过文档还不支持搜索功能&#xff0c;虽然目前内容不多&#xff0c;但待我同步完之后&#xff0c;搜索就很有必要了。 之前看 VitePress 官网发现没有…

pikachu靶场暴力破解绕过token防护详解

今天继续给大家介绍渗透测试相关知识&#xff0c;本文主要内容是pikachu靶场暴力破解绕过token防护详解。 免责声明&#xff1a; 本文所介绍的内容仅做学习交流使用&#xff0c;严禁利用文中技术进行非法行为&#xff0c;否则造成一切严重后果自负&#xff01; 再次强调&#x…

基于改进的多目标粒子群算法的微电网多目标调度(三个目标函数)(matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

中央重磅文件明确互联网医疗服务可用医保支付!

文章目录中央重磅文件明确互联网医疗服务可用医保支付&#xff01;中央重磅文件明确互联网医疗服务可用医保支付&#xff01; 当下&#xff0c;互联网医疗机构已加入到新冠防治的“主战场”&#xff0c;在分流线下诊疗发挥了很大作用。国家层面也在进一步鼓励互联网医疗行业发…

基于多尺度形态学梯度进行边缘检测(Matlab代码实现)

&#x1f468;‍&#x1f393;个人主页&#xff1a;研学社的博客 &#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜…

C++中的继承

把握住自己能把握住的点滴&#xff0c;把它做到极致&#xff0c;加油&#xff01; 本节目标1.继承的概念及定义1.1继承的概念1.2 继承定义1.2.1 定义格式1.2.2 继承方式和访问限定符1.2.3 继承基类成员访问方式的变化2.继承中的作用域练习3.基类和派生类对象赋值转换4.派生类的…

Java+SSM网上订餐系统点餐餐厅系统(含源码+论文+答辩PPT等)

项目功能简介: 该项目采用的技术实现如下 后台框架&#xff1a;Spring、SpringMVC、MyBatis UI界面&#xff1a;BootStrap、H-ui 、JSP 数据库&#xff1a;MySQL 系统功能 系统分为前台订餐和后台管理&#xff1a; 1.前台订餐 用户注册、用户登录、我的购物车、我的订单 商品列…

Linux 常用的命令

前言 Linux 的学习对于一个程序员的重要性是不言而喻的。前端开发相比后端开发&#xff0c;接触 Linux 机会相对较少&#xff0c;因此往往容易忽视它。但是学好它却是程序员必备修养之一。 作者使用的是阿里云服务器 ECS &#xff08;最便宜的那种&#xff09; CentOS 7.7 64…

快速了解JSON及JSON的使用

文章目录JSON简介JSON语法JSON 名称/值对JSON对象数组JSON的简单使用JSON简介 JSON&#xff08;JavaScriptObjectNotation&#xff0c;JS对象简谱&#xff09;是一种轻量级的数据交换格式 JS对象简谱&#xff0c;那么JSON如何转换为JS对象&#xff1a; JSON文本格式在语法上与…

多弹协同攻击时的无源定位

题目 采用被动接收方式的无源探测定位技术具有作用距离远、隐蔽接 收、不易被敌方发觉等优点&#xff0c;能有效提高探测系统在电子战环境下的 生存能力和作战能力。 在无源定位的研究中&#xff0c;测向定位技术&#xff08;Direction of Arrival&#xff0c;DOA&#xff09; …

SpringBoot操作Mongo

文章目录引入依赖yaml实体类集合操作创建删除相关注解文档操作添加实验 数据查询添加更新删除引入依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId> </dependency><de…

Jmeter配置不同业务请求比例,应对综合场景压测

背景 在进行综合场景压测时&#xff0c;遇到了如何实现不同的请求所占比例不同的问题。 有人说将这些请求分别放到单独的线程组下&#xff0c;然后将线程组的线程数按照比例进行配置。 这种方法不是很好&#xff0c;因为服务器对不同的请求处理能力不同&#xff0c;有的处理快…

C规范编辑笔记(八)

往期文章&#xff1a; C规范编辑笔记(一) C规范编辑笔记(二) C规范编辑笔记(三) C规范编辑笔记(四) C规范编辑笔记(五) C规范编辑笔记(六) C规范编辑笔记(七) 正文&#xff1a; 今天来给大家分享我们的第八篇C规范编辑笔记&#xff0c;话不多说&#xff0c;我们直接来看&…