当前位置: 首页 > 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的迷宫中找到出口的&…...

JavaSec-RCE

简介 RCE(Remote Code Execution)&#xff0c;可以分为:命令注入(Command Injection)、代码注入(Code Injection) 代码注入 1.漏洞场景&#xff1a;Groovy代码注入 Groovy是一种基于JVM的动态语言&#xff0c;语法简洁&#xff0c;支持闭包、动态类型和Java互操作性&#xff0c…...

《Playwright:微软的自动化测试工具详解》

Playwright 简介:声明内容来自网络&#xff0c;将内容拼接整理出来的文档 Playwright 是微软开发的自动化测试工具&#xff0c;支持 Chrome、Firefox、Safari 等主流浏览器&#xff0c;提供多语言 API&#xff08;Python、JavaScript、Java、.NET&#xff09;。它的特点包括&a…...

visual studio 2022更改主题为深色

visual studio 2022更改主题为深色 点击visual studio 上方的 工具-> 选项 在选项窗口中&#xff0c;选择 环境 -> 常规 &#xff0c;将其中的颜色主题改成深色 点击确定&#xff0c;更改完成...

令牌桶 滑动窗口->限流 分布式信号量->限并发的原理 lua脚本分析介绍

文章目录 前言限流限制并发的实际理解限流令牌桶代码实现结果分析令牌桶lua的模拟实现原理总结&#xff1a; 滑动窗口代码实现结果分析lua脚本原理解析 限并发分布式信号量代码实现结果分析lua脚本实现原理 双注解去实现限流 并发结果分析&#xff1a; 实际业务去理解体会统一注…...

【OSG学习笔记】Day 16: 骨骼动画与蒙皮(osgAnimation)

骨骼动画基础 骨骼动画是 3D 计算机图形中常用的技术&#xff0c;它通过以下两个主要组件实现角色动画。 骨骼系统 (Skeleton)&#xff1a;由层级结构的骨头组成&#xff0c;类似于人体骨骼蒙皮 (Mesh Skinning)&#xff1a;将模型网格顶点绑定到骨骼上&#xff0c;使骨骼移动…...

OpenPrompt 和直接对提示词的嵌入向量进行训练有什么区别

OpenPrompt 和直接对提示词的嵌入向量进行训练有什么区别 直接训练提示词嵌入向量的核心区别 您提到的代码: prompt_embedding = initial_embedding.clone().requires_grad_(True) optimizer = torch.optim.Adam([prompt_embedding...

C++ Visual Studio 2017厂商给的源码没有.sln文件 易兆微芯片下载工具加开机动画下载。

1.先用Visual Studio 2017打开Yichip YC31xx loader.vcxproj&#xff0c;再用Visual Studio 2022打开。再保侟就有.sln文件了。 易兆微芯片下载工具加开机动画下载 ExtraDownloadFile1Info.\logo.bin|0|0|10D2000|0 MFC应用兼容CMD 在BOOL CYichipYC31xxloaderDlg::OnIni…...

Java多线程实现之Thread类深度解析

Java多线程实现之Thread类深度解析 一、多线程基础概念1.1 什么是线程1.2 多线程的优势1.3 Java多线程模型 二、Thread类的基本结构与构造函数2.1 Thread类的继承关系2.2 构造函数 三、创建和启动线程3.1 继承Thread类创建线程3.2 实现Runnable接口创建线程 四、Thread类的核心…...

Yolov8 目标检测蒸馏学习记录

yolov8系列模型蒸馏基本流程&#xff0c;代码下载&#xff1a;这里本人提交了一个demo:djdll/Yolov8_Distillation: Yolov8轻量化_蒸馏代码实现 在轻量化模型设计中&#xff0c;**知识蒸馏&#xff08;Knowledge Distillation&#xff09;**被广泛应用&#xff0c;作为提升模型…...

安宝特案例丨Vuzix AR智能眼镜集成专业软件,助力卢森堡医院药房转型,赢得辉瑞创新奖

在Vuzix M400 AR智能眼镜的助力下&#xff0c;卢森堡罗伯特舒曼医院&#xff08;the Robert Schuman Hospitals, HRS&#xff09;凭借在无菌制剂生产流程中引入增强现实技术&#xff08;AR&#xff09;创新项目&#xff0c;荣获了2024年6月7日由卢森堡医院药剂师协会&#xff0…...