Aho-Corasick automaton,ac自动机实现

news2024/11/28 20:45:52

文章目录

    • 写在前面
    • 算法概述
    • trie树的构建
      • trie树的节点结构
      • 插入P串到trie树中
      • fail指针的创建
    • 搜索过程
    • 测试程序

写在前面

原作者的视频讲解链接:[算法]轻松掌握ac自动机_哔哩哔哩_bilibili
原作者的代码实现:data-structure-and-algorithm/aho_corasick.cpp at master · xiaoyazi333/data-structure-and-algorithm · GitHub
我的代码实现:data_structure/Aho-Corasick automaton/Aho-Corasick automaton · sacajawea/code_store2023 - 码云 - 开源中国 (gitee.com)
文章中的截图也来自原作者的视频

这篇文章是对上面视频的总结,原作者讲解的不错,看完一遍就能明白该算法的原理。只不过有些细节需要自己下来推敲一遍,为透彻的理解这个算法,我用C++重写了原作者的代码,并将思路总结成这篇文章。

算法概述

首先是两个概念的说明:

  • P串:pattern string,用来搜索的字符串
  • T串:text string,被搜索的文本串

AC自动机,这是一种多模式匹配算法,根据多个P串构建trie树,和KMP不同的是,只需要用T串遍历一次trie树,就可以知道哪些P串是在T串中出现过。
image.png

视频中有这样一个画面,这是ac自动机最重要的部分:fail指针。
image.png

fail指针:i->fail->j
word[j]是word[i]的最长后缀,什么是word[i]呢?

其表示从根节点出发到i节点构成的字符串。若fail指针指向根节点,说明该字符串没有最长后缀。若节点i的fail指针指向根节点以外的节点,假设是节点j,则说明word[i]的最长后缀是word[j]。

以上内容将在后续详细讲解。将视角拉高,回到ac自动机的整体结构上。

ac自动机可分为三个部分:

  1. 将所有P串插入到trie树中
  2. 为所有节点构建fail指针
  3. 根据T串遍历trie树,查找匹配的P串

trie树的构建

trie树的节点结构

由于我们要做的是字符串匹配,字符包括字母、特殊字母、数字等,字符的数量无法确定。要如何表示一个节点的子节点?这里用映射表unordered_map存储子节点表示字符与子节点地址的映射关系,映射表的成员个数就是该节点的子节点个数。

除了基本的指针域,trie树还要保存一个fail指针,以表示当前串的最长后缀。

同时,我们需要标记该节点是否是某一P串的结束,这里用vector<int>表示这一信息,数组存储的是某一P串的长度,只要vector<int>的长度不等于0,就说明从根节点到该节点表示一个P串。当然,从中间节点开始到该节点也可能表示一个P串,所以这里不能只存储一个int,而是要用vector存储多个int。

至于该节点表示哪个字符,这里用一个char变量保存。以下是节点的结构:

struct TrieNode
{
	std::vector<size_t> _exist;
	std::unordered_map<char, TrieNode*> _childs;
	TrieNode* _fail;
	char _name; 

	TrieNode(char name)
	{
		_fail = nullptr;
		_name = name;
	}
};

插入P串到trie树中

P串的第n个字符位于trie树的第n层,根节点位于树的第0层。

  • 每次插入前先检查这一字符对应的子节点是否已经构建
    • 若构建,则向该子节点遍历
    • 若没构建,则new一个新的节点并将其作为该节点的子节点
  • 最后将该串的长度填入最后一个节点的exist数组中
void TrieTree::_insert_pstr(const std::string& p_str)
{
	node* cur = _root;
	for (size_t i = 0; i < p_str.size(); ++i)
	{
		if (cur->_childs.find(p_str[i]) == cur->_childs.end())
			cur->_childs[p_str[i]] = new node(p_str[i]);

		cur = cur->_childs[p_str[i]];
	}
	// 字符对应的最后节点需要维护存在信息
	cur->_exist.push_back(p_str.size());
}

fail指针的创建

将所有P串插入到trie树中,此时trie树基本构建完成,但是还差最关键的一步:fail指针的创建。

先说明一些繁琐概念的别称:

  • X节点:当前遍历的节点
  • P节点:X节点的父节点
  • father_fail指针:P节点的fail指针
  • Y节点:X的fail指针指向的节点
  • PY节点:father_fail指针指向的节点
  • word[i]:从根节点到i节点构成的字符串

对于Y节点,其指向trie树中,从“根节点到X节点构成的字符串”的“最长后缀字符串的最后一个节点”。如何构建X的fail指针?可以通过father_fail指针,找到trie树中,从“根节点到P节点构成的字符串”的“最长后缀的最后一个节点PY”。只说概念太抽象了,举个例子:

比如this这个字符串

  • 假设X节点为字符s,当word[X] = "this"时
  • 那么P节点就是字符i,word[P] = “thi”
  • father_fail指针指向PY节点,word[PY]可能为"hi",也可能为"i"
    • 也可能word[P]没有最长后缀,此时PY节点指向根节点

当我们要找"this"的最长后缀(也就是找Y节点)时,先通过father_fail指针找到"thi"的后缀(注意,这里没有最长),如"hi",“i”,或者根节点。然后判断"hi",“i"往下是否能构成"his”,“is”。

  • 如果能构成,Y节点优先指向“较长后缀的最后一个节点”
  • 如果不能构成,Y节点指向根节点

如何判断"hi",“i"往下是否能构成"his”,“is”?具体做法是:通过判断PY节点的子节点中,是否存在表示字符和X节点表示字符相同的节点,在上面的例子中,就是判断PY节点是否存在表示字符s的子节点。

  • 若不存在,则尝试word[P]剩下的后缀字符串
    • 直到所有后缀尝试完(注意:不要忘记了空后缀,我们需要判断根节点的子节点中,是否存在表示字符和X节点表示字符相同的节点),说明word[P]没有一个后缀有对应子节点。此时X的fail指针将指向根节点,表示trie树没有word[X]的最长后缀
  • 若存在,X的fail指针指向该节点

如何知道trie树中word[P]的所有后缀字符串?很简单,一直遍历word[P]的最后一个节点的fail指针,直到该指针指向根节点,每个fail指针都表示一个后缀字符串,当fail指针指向根节点,此时表示word[P]的后缀字符串为空,后续没有word[P]的后缀了。

void TrieTree::_build_tree(const std::vector<std::string>& p_strs)
{
	// 将p串插入到trie树中
	for (size_t i = 0; i < p_strs.size(); ++i)
	{
		_insert_pstr(p_strs[i]);
	}

	// 层序遍历,先将第一层(根节点为第0层)的节点入队,第一层节点的fail指针指向根节点
	std::queue<node*> level;
	for (const auto& kv : _root->_childs)
	{
		node* child = kv.second;
		level.push(child);
		child->_fail = _root;
	}
		

	while (!level.empty())
	{
		node* cur_node = level.front();
		level.pop();
		
		// 构建node所有子节点的fail指针
		for (const auto& kv : cur_node->_childs)
		{
			char name = kv.first;
			node* child_node = kv.second;

			node* father_fail = cur_node->_fail;

			// 找word[P]的后缀,需要PY节点含有对应字符的子节点
			// 所以当PY节点没有对应字符的子节点时,需要更新PY节
			// PY节点也就是father_fail指向的节点
			// 当PY节点为nullptr
			while (father_fail != nullptr && father_fail->_childs.find(name) == father_fail->_childs.end())
				father_fail = father_fail->_fail;

			if (father_fail == nullptr)
				child_node->_fail = _root;
			else
				child_node->_fail = father_fail->_childs[name];

			// 若最长后缀也是一个P串,此时要维护exits数组
			for (size_t j = 0; j < child_node->_fail->_exist.size(); ++j)
				child_node->_exist.push_back(child_node->_fail->_exist[j]);

			level.push(child_node);
		}
	}
}

搜索过程

  • 从T串的第一个字符开始,从根节点遍历trie树。即当前节点为根节点,当前字符为T串的第一个字符
  • 因为根节点不表示任何字符,所以我们需要判断当前节点的“子节点表示的字符“是否和当前字符匹配
  • 若匹配,则向其遍历。判断该节点表示的字符是否是某一P串的最后字符
    • 若是,则根据exits数组存储的P串长度,在T串中截取P串,并保存
    • 若不是,什么都不做
  • 若不匹配,则更新当前节点为fail指针指向的节点,重复上面的判断

需要注意的是:根节点的fail指针指向nullptr,此时我们不能更新当前节点为nullptr。因为后续要解引用当前节点,获取其子节点信息,解引用nullptr是非法的。

若当前节点的fail指针指向nullptr,则说明该节点是根节点。若当前字符与根节点的子节点匹配失败,则说明trie树中没有以该字符作为起始字符的P串,此时更新当前字符为下一字符即可。

直到所有字符都经过匹配,搜索完成。

void TrieTree::_query_tstr(std::unordered_set<std::string>& res, const std::string& t_str)
{
	node* cur_node = _root;
	for (size_t i = 0; i < t_str.size(); ++i)
	{
		char cur_char = t_str[i];
		// 若遇到空格,从根节点开始重新遍历trie树
		if (cur_char == ' ')
		{
			cur_node = _root;
			continue;
		}

		// 找到当前字符对应的节点
		// 若当前节点没有对应子节点且当前节点不是根节点,向fail指针指向的节点遍历
		// 注意:fail指针是否为nullptr是区分根节点与其他节点的关键
		while (cur_node->_fail != nullptr && cur_node->_childs.find(cur_char) == cur_node->_childs.end())
			cur_node = cur_node->_fail;
		
		// 找不到字符对应的节点,跳过该字符
		if (cur_node->_childs.find(cur_char) == cur_node->_childs.end())
			continue;
		// 找到字符对应的节点,向其遍历
		else
			cur_node = cur_node->_childs[cur_char];

		// 若当前节点表示某些P串的最后一个字符,返回P串
		for (size_t j = 0; j < cur_node->_exist.size(); ++j)
		{
			size_t length = cur_node->_exist[j];
			res.insert(t_str.substr(i - length + 1, length));
		}
	}
}

为什么要使用unordered_set<string>存储P串结果,不能用vector<string>吗?其实以上算法可能导致同一P串被多次匹配,使得vector<string>保存重复的结果,所以我使用unordered_set进行去重。

测试程序

进行一些常规的测试,以检测代码是否能正常运行:

#include "Aho-Corasick.hpp"

void UtilTest(const std::vector<std::string> p_strs, const std::string& t_str)
{
	TrieTree tree;
	std::unordered_set<std::string> res;
	
	std::cout << "trie树中的P串:" << std::endl;
	for (const auto& p_str : p_strs)
	{
		std::cout << p_str << ' ';
	}
	std::cout << std::endl;

	tree._build_tree(p_strs);
	tree._query_tstr(res, t_str);
	std::cout << t_str << "中,存在的P串:" << std::endl;
	for (auto& str : res)
	{
		std::cout << str << ' ';
	}
	std::cout << std::endl << "-------------------------------------------------------------" << std::endl;
}


int main()
{
	std::vector<std::string> p_strs = { "apple", "banana", "pear" };
	UtilTest(p_strs, "An apple a day keeps the doctor away");
	UtilTest(p_strs, "An apple a day keeps the doctor away. I love bananas and pears too!");

	p_strs = { "at", "cat", "hat" };
	UtilTest(p_strs, "the cat in the hat sat on the mat");
	
	return 0;
}

测试结果:
image.png

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

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

相关文章

机器视觉表面划痕检测流程

表面缺陷检测常见的检测主要有物体表面的划痕、污渍、缺口、平面度、破损、边框对齐度、物体表面的亮度、皱纹、斑点、孔等。 表面缺陷检测设备凝聚了机器视觉领域的许多技术成果&#xff0c;吸取了许多创新的检测理念&#xff0c;可以与现有生产线无缝对接检测&#xff0c;也…

制定进度计划是成功项目管理的必由之路

项目经理王斌接到一个新项目&#xff0c;与各项目干系人没有建立有效的联系&#xff0c;他们无法了解项目进展情况。甚至连项目团队的参与人员自身对项目整体情况也没有清楚的认识&#xff0c;而只管自己那一部分&#xff0c;整个开发过程完全是一种黑盒模式&#xff0c;项目组…

电视盒子哪个好?内行盘点2023最具性价比电视盒子推荐

电视盒子跟有线机顶盒相比不用每年缴费&#xff0c;资源也更丰富&#xff0c;可下载各种APP。作为电视盒子从业人员&#xff0c;身边亲友在选购电视盒子之前会咨询我的意见&#xff0c;不懂电视盒子哪个好&#xff0c;可以看看我总结的2023最具性价比电视盒子推荐&#xff0c;非…

手慢无,阿里巴巴最新出品的高并发终极笔记到底有多强?

前几天收到一位粉丝私信&#xff0c;说的是他才一年半经验&#xff0c;去面试却被各种问到分布式&#xff0c;高并发&#xff0c;多线程之间的问题。基础层面上的是可以答上来&#xff0c;但是面试官深问的话就不会了&#xff01;被问得都怀疑现在Java招聘初级岗位到底招的是初…

MySQL-图形化界面工具 (下)

♥️作者&#xff1a;小刘在C站 ♥️个人主页&#xff1a;小刘主页 ♥️每天分享云计算网络运维课堂笔记&#xff0c;努力不一定有收获&#xff0c;但一定会有收获加油&#xff01;一起努力&#xff0c;共赴美好人生&#xff01; ♥️树高千尺&#xff0c;落叶归根人生不易&…

springboot+mybatis+redis实现二级缓存

Mybatis提供了对缓存的支持&#xff0c;分为一级缓存和二级缓存&#xff0c;其查询顺序为&#xff1a;二级缓存>一级缓存->数据库&#xff0c;最原始是直接查询数据库&#xff0c;为了提高效率和节省资源&#xff0c;引入了一级缓存&#xff0c;为了进一步提高效率&#…

计算机网络(四下)——网络层

接上篇&#xff0c;这篇文章主要来写路由选择 五、路由协议 1>动态路由 1.距离向量算法&#xff08;RIP协议&#xff09;&#xff1b;适用于小型网络 1》规定&#xff1a; 1>记录跳数(Hop count)最少的路径。 2>RIP允许一条路由最多15个路由器&#xff0c;距离为…

LitCTF2023 wp re最后一道 cry misc

本来不打算放了&#xff0c;但是比赛打都打了留个纪念社工有佬&#xff0c;与我无关&#xff0c;misc只会隐写虽然我是逆向手&#xff0c;但因为队友tql&#xff0c;所以只留给我最后一道~~我的wp向来以简述思路为主&#xff0c;习惯就好 Crypto Hex&#xff1f;Hex&#xff…

【项目设计】 负载均衡在线OJ系统

&#x1f9f8;&#x1f9f8;&#x1f9f8;各位大佬大家好&#xff0c;我是猪皮兄弟&#x1f9f8;&#x1f9f8;&#x1f9f8; 文章目录 一、项目介绍项目技术栈和开发环境 二、项目的宏观结构三、compile_server模块①日志模块开发&#xff0c;Util工具类&#xff0c;供所以模…

NVIDIA再现谜之刀法,RTX 4060Ti新增16G版

随着上一代库存逐渐清理到位&#xff0c;苏妈与老黄终于要把新一代主流级显卡掏出来了。 根据外网消息&#xff0c;AMD 这边主要是 RX 7600XT 与 7600 等型号&#xff0c;发布日期定为 5 月 25 日。 AMD 保密措施做得挺到位的&#xff0c;目前除了部分厂商爆出的包装与产品图…

[MYAQL / Mariadb] 数据库学习-数据导入导出

数据库学习-数据导入导出 数据导入导出&#xff08;批量处理数据&#xff09;查看默认检索目录模糊查询&#xff1a;show variables like %XXXX%;修改检索目录路径&#xff08;&#xff01;&#xff01;文件一定要有MySQL用户的 7的RWX 权限&#xff01;&#xff09;默认的检索…

前端-02 CSS基础

1 简介 1.1 CSS语法 语法 选择器&#xff1a;HTML元素 生命块&#xff1a;用;隔开的各种声明 {a;b} 每条声明有CSS属性名称和值&#xff0c;用冒号分割{属性:值;属性:值} 案例 整块代码 <!DOCTYPE html> <html><head><style>body {background…

同一个IP可以安装配置多个SSL证书吗?

如何在同一IP地址上运行多个SSL证书? 服务器名称指示SNI&#xff0c;可以帮助您实现同一IP运行多个SSL证书&#xff0c;这样虚拟主机网站也能用上SSL证书了。 什么是SNI 服务器名称指示SNI是SSL的一个重要组成部分&#xff0c;SNI允许多个网站存在于同一个IP地址上&#xff…

CVPR目标检测经典作:HOG特征

来源&#xff1a;投稿 作者&#xff1a;小灰灰 编辑&#xff1a;学姐 HOG特征 HOG特征( Histogram of Oriented Gradients 方向梯度直方图&#xff09;是一种在图像上找到特征描述子&#xff0c;主要通过计算和统计图像局部区域的梯度方向直方图来构成特征。来源于cvpr2015 年…

Angular 学习笔记

本系列笔记主要参考&#xff1a; Angular学习视频 Angular官方文档 Angular系列笔记 特此感谢&#xff01; 目录 1.Angular 介绍2.Angular 环境搭建、创建 Angular 项目、运行 Angular 项目2.1.环境搭建2.2.创建 Angular 项目2.3.运行项目 3.Angular 目录结构分析3.1.目录结构分…

低分辨率视频可以变高分辨率吗?

近几年&#xff0c;老电影、老视频片段修复越来越常见了。很多优质的片源&#xff0c;因为年代久远&#xff0c;分辨率较低&#xff0c;画质比较差&#xff0c;通过视频超分技术&#xff0c;实现了画质增强&#xff0c;提高画质分辨率&#xff0c;视频画面变得更清晰了。 首先…

计算机有哪些方面的技术? - 易智编译EaseEditing

计算机是一种多功能的电子设备&#xff0c;可以处理数据、进行信息存储和检索、进行计算和模拟等多种任务。计算机技术是指计算机相关的技术领域&#xff0c;包括硬件和软件等多个方面。下面介绍一些常见的计算机技术&#xff1a; 操作系统技术&#xff1a; 操作系统是计算机系…

select poll epoll有什么区别

select/poll select 实现多路复用的方式是&#xff0c;将已连接的 Socket 都放到一个文件描述符集合&#xff0c;然后调用 select 函数将文件描述符集合拷贝到内核里&#xff0c;让内核来检查是否有网络事件产生&#xff0c;检查的方式很粗暴&#xff0c;就是通过遍历文件描述…

什么是日志文件

文章目录 什么是日志文件Centos 7 日志文件简易说明日志文件的重要性Linux常见的日志文件文件名/var/log/boot.log/var/log/cron/var/log/dmesg/var/log/lastlog/var/log/maillog或 /var/log/mail/*/var/log/messages/var/log/secure/var/log/wtmp、/var/log/faillog/var/log/h…

Netty实战(二)

第一个Netty程序 一、环境准备二、Netty 客户端/服务器概览三、编写 Echo 服务器3.1 ChannelHandler 和业务逻辑3.2 引导服务器 四、编写 Echo 客户端4.1 通过 ChannelHandler 实现客户端逻辑4.2 引导客户端 五、构建和运行 Echo 服务器和客户端 一、环境准备 Netty需要的运行…