当前位置: 首页 > news >正文

「字符串」详解AC自动机并实现对应的功能 / 手撕数据结构(C++)

目录

前置知识

概述

核心概念:fail指针

作用

构建

图示

Code

成员变量

创建销毁

添加词库

文本扫描

复杂度 

Code


前置知识

在此前,你应该首先了解trie树(字典树)的概念:

「字符串」详解Trie(字典树|前缀树)并实现对应的功能 / 手撕数据结构(C++)

我们建议你先阅读这篇文章,以了解我们实现字典树所用的一些结构,比如trie_node,以及词尾节点存储了整个字符串这些概念。


概述

AC自动机是能以线性时间复杂度对整个文本进行黑名单词汇统计的数据结构。

我们先将黑名单语句词库逐条插入这棵字典树。当我们扫描文本时,就能以一次遍历实现对文本中所有出现的黑名单库中的语句进行统计。

你应该这样认识AC自动机:它首先是一颗字典树,其次是它还有一个成员:fail指针数组

就如同他的名字一样,这个自动机结构会根据输入的内容自动地调整内部的行为,这就是它能以单次扫描就匹配全部文本的奥妙之处。


核心概念:fail指针

作为一颗独特的字典树,AC自动机还有fail指针数组。

作用

它的定义是:trie_node* fail[i];

fail指针,顾名思议,它会在匹配失败时指向另一个可行的节点。

这个数组是干什么的?你可以认为如果我们的匹配失败,那么fail指针就会启动,并跳转到一个具有和当前节点相同字符的节点上,例如fail[5]存储了与5号节点具有相同字符的节点地址。

这样讲似乎很让人不能理解,我们来看这张图:

预先插入了以上敏感词字符串。如果我们要匹配的是“sher”,如果进行朴素匹配那么我们只能得到“she”,但我们知道有三个字符串“she”“he”“her”都在其中,我们怎么才能只扫描一次就得到这些字符串呢?fail指针做了这件事。

当我们匹配到字符'r'时,发现无法继续,此时fail指针发力了,由于位于5号节点,而fail[5]->8号节点的地址,所以我们可以跳转到8号节点继续匹配。

这样一来“she”“he”“her”就全部匹配上了。

下面我们来说说fail指针是怎么构建的。

构建

有三条原则:

1.root节点的fail指针指向自己,此后按层遍历,进行各个层次的fail指针的构建。

2.如果某节点A的fail指针指向另一非root节点B,那么A的字符与B的字符是相同的。如fail[5]->8号节点。

3.如果当前节点child1的父节点father1的fail指针指向另一节点father2,而father2名下恰好有与child1同字符的child2,那么fail[child1]->child2,如果没有同字符的child2,那么fail[child1]->root。

我们重点解释第三条:这是为了保证我们的匹配是有效的:

如果fail[father1]->father2,那么father1与father2是相同字符节点,那么可以保证从child1通过fail指针跳转到child2时,他们前面匹配过得一部分字符串(即father1和father2)是相同的,这样一来,可以保证在我们进行文本匹配时,不会出现3号节点跳转到6号节点(看上图)的现象。

*注意*:也有child1跳转到child2时不存在前面匹配过的一部分字符串,即child1的字符可能是某个敏感词的首字符,这时候跳转到root进行匹配(即第三条规则的最后一句话)。

图示

我们希望你结合以上三条规则在图上画出全部fail指针,在此我们给出答案:

Code

node->idx代表节点编号,它同时担任了fail指针数组下标的功能。

std::vector <trie_node*> fail;
void bulid_fail() {fail.resize(val_size,root);std::queue<trie_node*>que;que.push(root);while (!que.empty()) {int len = que.size();while (len--) {trie_node* node = que.front(); que.pop();for (int i = 0; i < branches; i++) {trie_node*& father= node;trie_node*& child = node->next[i];if (child != nullptr) {que.push(child);if (fail[father->idx]->next[i] != nullptr&&fail[father->idx]->next[i]!=child)fail[child->idx] = fail[father->idx]->next[i];}}}}//int i = 0;//for (const trie_node* node : fail)std::cout << i++ << "->" << node->idx << std::endl;
}

接下来,我们封装AC_automaton类,实现AC自动机的基本功能。(Code和测试案例附后)


成员变量

ac自动机以私有方式继承trie树,并封装std::vector <trie_node*> fail指针。

class AC_automaton:private trie{
private:std::vector <trie_node*> fail;...
public:...}
};

创建销毁

提供三种构造,一种析构函数,。

这些内容均于「字符串」详解Trie(字典树|前缀树)并实现对应的功能 / 手撕数据结构(C++)中提及。

我们删除了复制构造和复制赋值等于号,以防止两份AC自动机的fail指针指向同一组地址。

AC_automaton(int branch = 128) :trie(branch), fail(1, root) {};
AC_automaton(const trie& data) :trie(data), fail(1,root) {bulid_fail();
};
AC_automaton(const AC_automaton& another) = delete;
AC_automaton(const std::initializer_list<std::string> ini_list) :trie(ini_list), fail(1, root) {bulid_fail();
}
~AC_automaton() {};
AC_automaton& operator=(const AC_automaton& another) = delete;

添加词库

提供insert_blackedlist与它的重载,支持单条语句和多语句插入。

随后调用build_fail重建fail指针。

void insert_blacklist(const std::string str) {trie::insert(str);bulid_fail();
}
void insert_blacklist(const std::vector<std::string> strs) {trie::insert(strs);bulid_fail();
}

文本扫描

文本扫描是AC自动机的另一个核心部分,它是fail指针的具体应用。

初始化指针p指向根节点。text为待扫描文本。

对于这个流程,我们总结为以下几点:

1.当p指向根节点并且黑名单字符串库不存在当前text[i]开头的字符,那么跳过当前字符。

2.当p指针指向的节点储存了str黑名单字符串,将其加入统计结果。

3.当p指针无法继续向下匹配,启动fail指针,前往fail[p->idx](p->idx表示当前节点的序号,fail[p->idx]存储当前节点的跳转位置)

*注意*:因为跳转后的节点字符与跳转前的节点字符相同,此时请不要向后移动text文本字符,即不要进行i++操作,这会导致匹配错位。

4.脱离循环后将p位置可能存在的字符串加入统计结果,以及p位置的fail指针指向的节点也可能有字符串,他们都需要加入统计结果。这是由于p抵达最后一个字符时循环已经结束,并且fail指针有着p的相同前缀以及相同字符,他们都有可能成为统计结果。

std::vector<std::string> query(std::string text) {std::vector<std::string> ans;trie_node* p = root;const int len = text.size();for (int i = 0; i < len;) {if (p == root && p->next[text[i]] == nullptr)i++;if (p->str.empty()==false)ans.push_back(p->str);if (p->next[text[i]] != nullptr)p = p->next[text[i++]];else p = fail[p->idx];}if (p->str.empty() == false)ans.push_back(p->str);if(fail[p->idx]->str.empty()==false); ans.push_back(fail[p->idx]->str);return ans;
}

复杂度 

时间复杂度:插入:O(n*m) 扫描:O(m)

空间复杂度:插入:O(n*m) 扫描:O(1)

n:插入字符串数目

m:插入/待扫描字符串长度


Code

#include <queue>
#include "trie.h"
#ifndef AC_AUTOMATON
#define AC_AUTOMATON
class AC_automaton:private trie{
private:std::vector <trie_node*> fail;void bulid_fail() {fail.resize(val_size,root);std::queue<trie_node*>que;que.push(root);while (!que.empty()) {int len = que.size();while (len--) {trie_node* node = que.front(); que.pop();for (int i = 0; i < branches; i++) {trie_node*& father= node;trie_node*& child = node->next[i];if (child != nullptr) {que.push(child);if (fail[father->idx]->next[i] != nullptr&&fail[father->idx]->next[i]!=child)fail[child->idx] = fail[father->idx]->next[i];}}}}//int i = 0;//for (const trie_node* node : fail)std::cout << i++ << "->" << node->idx << std::endl;}
public:AC_automaton(int branch = 128) :trie(branch), fail(1, root) {};AC_automaton(const trie& data) :trie(data), fail(1,root) {bulid_fail();};AC_automaton(const AC_automaton& another) = delete;AC_automaton(const std::initializer_list<std::string> ini_list) :trie(ini_list), fail(1, root) {bulid_fail();}~AC_automaton() {};AC_automaton& operator=(const AC_automaton& another) = delete;void insert_blacklist(const std::string str) {trie::insert(str);bulid_fail();}void insert_blacklist(const std::vector<std::string> strs) {trie::insert(strs);bulid_fail();}std::vector<std::string> query(std::string text) {std::vector<std::string> ans;trie_node* p = root;const int len = text.size();for (int i = 0; i < len;) {if (p == root && p->next[text[i]] == nullptr)i++;if (p->str.empty()==false)ans.push_back(p->str);if (p->next[text[i]] != nullptr)p = p->next[text[i++]];else p = fail[p->idx];}if (p->str.empty() == false)ans.push_back(p->str);if(fail[p->idx]->str.empty()==false); ans.push_back(fail[p->idx]->str);return ans;}
};
#endif

测试 

#include <iostream>
#include "AC_automaton.h"
using namespace std;
int AC_automaton_test() {AC_automaton ACA = { "say","she","shy","he","her","hee","ee"};vector<string>&& ans = ACA.query("sherhee");cout << "-----------------test-----------------" << endl;for (const string& str : ans)cout << str << endl;;return 0;
}

相关文章:

「字符串」详解AC自动机并实现对应的功能 / 手撕数据结构(C++)

目录 前置知识 概述 核心概念&#xff1a;fail指针 作用 构建 图示 Code 成员变量 创建销毁 添加词库 文本扫描 复杂度 Code 前置知识 在此前&#xff0c;你应该首先了解trie树&#xff08;字典树&#xff09;的概念&#xff1a; 「字符串」详解Trie&#xff0…...

freecad遭遇网络不同无法安装插件Addon Manager: Unexpected 0 response from server

16:31:18 Addon Manager: Unexpected 0 response from server 16:31:18 Failed to connect to GitHub. Check your connection and proxy settings. 打开freecad的插件管理器时候&#xff0c;有些地方&#xff0c;比如我在家里就不行&#xff0c;在公司就ok。 于是找到了解…...

Ruby模板引擎:构建动态视图的艺术

标题&#xff1a;Ruby模板引擎&#xff1a;构建动态视图的艺术 在Ruby on Rails的世界里&#xff0c;模板引擎是构建动态网页的基石。它们允许开发者将服务器端的逻辑嵌入到HTML中&#xff0c;实现数据的动态展示。本文将深入探讨Ruby中几种常用的模板引擎&#xff0c;包括ERB…...

HarmonyOS NEXT星河版零基础入门(3)

目录 1. 系统弹出框 2.interface转成class类 3.vp/fp 4. 写一个正方形 设置它的宽度 但不设定高度 不论屏幕怎么变实现他的宽高比 5.State 6.图片和资源 7.淘宝镜像 7.1windows 脚本禁用&#xff08;操作策略 允许npm包的命令可执行&#xff09; 8. es6&ArkTS中…...

第二十讲 python中的异常结构-try except-else-finally

目录 1.try... except 结构 2. try... 多个except结构 3. try...except...else结构 4. try...except...finally结构 5. return语句和异常处理问题 5.1 异常处理前的 return 5.2异常处理后的 return 5.3 finally 块中的 return 6.常见的异常 1.try... except 结构 try except 是…...

springer 投稿系统中返修注意点

初次提交 初次提交时&#xff0c; manuscript 提交的是 pdf 文件 返修后提交 在经过返修之后需要提交的是注意一下几点&#xff1a; 此时提交的Blined manuscript &#xff0c;虽然名字没变&#xff0c;但不能再提交pdf 文件&#xff0c; 而需要提交的是可编辑的源文件 .te…...

CSS:display和visiblity

隐藏元素- display:none和visibility:hidden display 属性设置一个元素应如何显示&#xff0c;visibility 属性指定一个元素应可见还是隐藏。 隐藏一个元素可以通过吧display属性设置为“none”&#xff0c;或者把visibility属性设置为“hidden”。但是这两种会产生不同的结果…...

43.x86游戏实战-XXX寻找吸怪坐标

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 本次游戏没法给 内容参考于&#xff1a;微尘网络安全 工具下载&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1rEEJnt85npn7N38Ai0_F2Q?pwd6tw3 提…...

Redis地理位置相关应用

下面是一个结合 MySQL 数据库和 Redis 的地理位置服务示例&#xff0c;包含表结构、PHP 代码和 Redis 操作&#xff0c;用于处理基于地理位置的数据存储和查询。 1. 创建 MySQL 数据库表 首先&#xff0c;创建一个用于存储位置信息的 MySQL 表&#xff0c;如下所示&#xff1…...

优化WAN流量:如何通过调整系统设置降低企业网络成本

一、症状与问题背景 当电脑显示空闲状态时&#xff0c;如果满足以下条件&#xff0c;第二拨号链接可能会意外激活&#xff1a; 您正在使用基于 Microsoft Windows 的计算机&#xff0c;该计算机连接到远程网络并且是 Active Directory 域服务 (AD DS) 域的成员。 您通过二级…...

Java-HttpHeaders请求头或响应头

HttpHeaders 是 Spring Framework 中的一个类,用于封装 HTTP 头部信息。它提供了一种方便的 方式来设置 HTTP 请求头和处理 HTTP 响应头。下面分别介绍如何使用 HttpHeaders 来设置请求 头和处理响应头。 设置请求头 在发送 HTTP 请求时,可以通过 HttpHeaders 设置各种请…...

Elasticsearch高阶查询

Elasticsearch高阶查询 文章目录 Elasticsearch高阶查询相关性和相关性算分相关性 (Relevance)什么是TF-IDFBM25explain关键字Boosting如何通过Boost控制想要的文档排在前面&#xff1f; 布尔查询&#xff08;bool Query&#xff09;查询语法语法格式 单字符串多字段查询三种场…...

【流媒体】RTMPDump—RTMP_Connect函数(握手、网络连接)

目录 1. RTMP_Connect函数1.1 网络层连接&#xff08;RTMP_Connect0&#xff09;1.2 RTMP连接&#xff08;RTMP_Connect1&#xff09;1.2.1 握手&#xff08;HandShake&#xff09;1.2.2 RTMP的NetConnection&#xff08;SendConnectPacket&#xff09; 2.小结 RTMP协议相关&am…...

通过https方式访问内网IP

单位要做个用浏览器扫二维码的功能。我先在本地测试一直不成功&#xff0c;后来放到服务器上运行成功了。比较了一下&#xff0c;服务器上是https&#xff0c;但是本地没有证书。我问了一下信安的同事&#xff0c;要求二维码必须在本地扫描&#xff0c;不能上公网。所以只好在本…...

flutter 键盘弹出 都会重新Build

原因是调用MediaQuery.of(context)后&#xff0c;点击TextField组件时会导致调用build方法。 解决方法&#xff1a;在Scaffold组件的body嵌套Builder组件&#xff0c;然后设置一个BuildContext变量&#xff0c;将Builder组件中的context传递给BuildContext变量&#xff0c;然后…...

RedisDistributedLock 分布式锁

设计一个简单的 RedisDistributedLock 类&#xff0c;实现单例模式&#xff0c;并包含基本的锁定机制。这个类将使用 Redis 来管理锁&#xff0c;确保在分布式系统中资源的同步访问 import redis.clients.jedis.Jedis;public class RedisDistributedLock {private static Redi…...

Java之包装类

Java中的包装类&#xff08;Wrapper Classes&#xff09;是基本数据类型的对象包装类。Java为每个基本数据类型&#xff08;如int、char等&#xff09;提供了对应的包装类&#xff0c;使得基本类型可以被当作对象来处理。这些包装类位于java.lang包中。 包装类的用途 对象化&a…...

Linux - 权限

文章目录 一、用户二、文件 一、用户 1、Linux下有两种用户&#xff1a;超级用户&#xff08;root&#xff09;、普通用户。 超级用户&#xff1a;可以再linux系统下做任何事情&#xff0c;不受限制 。 普通用户&#xff1a;在linux下做有限的事情。 超级用户的命令提示符是“…...

免费图形化nginx管理工具nginxWebUI

nginxWebUI是一款图形化管理nginx配置得工具, 可以使用网页来快速配置nginx的各项功能, 包括http协议转发, tcp协议转发, 反向代理, 负载均衡, 静态html服务器, ssl证书自动申请、续签、配置等, 配置好后可一建生成nginx.conf文件, 同时可控制nginx使用此文件进行启动与重载, 完…...

编程上的挫折不可怕,可怕的是你畏惧了

如何克服编程学习中的挫折感 编程学习之路上&#xff0c;挫折感就像一道道难以逾越的高墙&#xff0c;让许多人望而却步。然而&#xff0c;真正的编程高手都曾在这条路上跌倒过、迷茫过&#xff0c;却最终找到了突破的方法。那么&#xff0c;我是如何在Bug的迷宫中找到出口的&…...

国防科技大学计算机基础课程笔记02信息编码

1.机内码和国标码 国标码就是我们非常熟悉的这个GB2312,但是因为都是16进制&#xff0c;因此这个了16进制的数据既可以翻译成为这个机器码&#xff0c;也可以翻译成为这个国标码&#xff0c;所以这个时候很容易会出现这个歧义的情况&#xff1b; 因此&#xff0c;我们的这个国…...

大数据学习栈记——Neo4j的安装与使用

本文介绍图数据库Neofj的安装与使用&#xff0c;操作系统&#xff1a;Ubuntu24.04&#xff0c;Neofj版本&#xff1a;2025.04.0。 Apt安装 Neofj可以进行官网安装&#xff1a;Neo4j Deployment Center - Graph Database & Analytics 我这里安装是添加软件源的方法 最新版…...

手游刚开服就被攻击怎么办?如何防御DDoS?

开服初期是手游最脆弱的阶段&#xff0c;极易成为DDoS攻击的目标。一旦遭遇攻击&#xff0c;可能导致服务器瘫痪、玩家流失&#xff0c;甚至造成巨大经济损失。本文为开发者提供一套简洁有效的应急与防御方案&#xff0c;帮助快速应对并构建长期防护体系。 一、遭遇攻击的紧急应…...

K8S认证|CKS题库+答案| 11. AppArmor

目录 11. AppArmor 免费获取并激活 CKA_v1.31_模拟系统 题目 开始操作&#xff1a; 1&#xff09;、切换集群 2&#xff09;、切换节点 3&#xff09;、切换到 apparmor 的目录 4&#xff09;、执行 apparmor 策略模块 5&#xff09;、修改 pod 文件 6&#xff09;、…...

对WWDC 2025 Keynote 内容的预测

借助我们以往对苹果公司发展路径的深入研究经验&#xff0c;以及大语言模型的分析能力&#xff0c;我们系统梳理了多年来苹果 WWDC 主题演讲的规律。在 WWDC 2025 即将揭幕之际&#xff0c;我们让 ChatGPT 对今年的 Keynote 内容进行了一个初步预测&#xff0c;聊作存档。等到明…...

srs linux

下载编译运行 git clone https:///ossrs/srs.git ./configure --h265on make 编译完成后即可启动SRS # 启动 ./objs/srs -c conf/srs.conf # 查看日志 tail -n 30 -f ./objs/srs.log 开放端口 默认RTMP接收推流端口是1935&#xff0c;SRS管理页面端口是8080&#xff0c;可…...

基于Docker Compose部署Java微服务项目

一. 创建根项目 根项目&#xff08;父项目&#xff09;主要用于依赖管理 一些需要注意的点&#xff1a; 打包方式需要为 pom<modules>里需要注册子模块不要引入maven的打包插件&#xff0c;否则打包时会出问题 <?xml version"1.0" encoding"UTF-8…...

【配置 YOLOX 用于按目录分类的图片数据集】

现在的图标点选越来越多&#xff0c;如何一步解决&#xff0c;采用 YOLOX 目标检测模式则可以轻松解决 要在 YOLOX 中使用按目录分类的图片数据集&#xff08;每个目录代表一个类别&#xff0c;目录下是该类别的所有图片&#xff09;&#xff0c;你需要进行以下配置步骤&#x…...

三体问题详解

从物理学角度&#xff0c;三体问题之所以不稳定&#xff0c;是因为三个天体在万有引力作用下相互作用&#xff0c;形成一个非线性耦合系统。我们可以从牛顿经典力学出发&#xff0c;列出具体的运动方程&#xff0c;并说明为何这个系统本质上是混沌的&#xff0c;无法得到一般解…...

作为测试我们应该关注redis哪些方面

1、功能测试 数据结构操作&#xff1a;验证字符串、列表、哈希、集合和有序的基本操作是否正确 持久化&#xff1a;测试aof和aof持久化机制&#xff0c;确保数据在开启后正确恢复。 事务&#xff1a;检查事务的原子性和回滚机制。 发布订阅&#xff1a;确保消息正确传递。 2、性…...