Aho-Corasick automaton,ac自动机实现

文章目录

    • 写在前面
    • 算法概述
    • 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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

matmul/mm 函数用法介绍

介绍torch.matmul之前先介绍torch.mm函数, mm和matmul都是torch中矩阵乘法函数&#xff0c;mm只能作用于二维矩阵&#xff0c;matmul可以作用于二维也能作用于高维矩阵 mm函数使用 x torch.rand(4, 9) y torch.rand(9, 8) print(torch.mm(x,y).shape)torch.Size([4, 8]) m…

OpenAI-whisper语音识别模型

1、whisper简介 Whisper是一个通用的语音识别模型。它是在不同音频的大型数据集上训练的&#xff0c;也是一个多任务模型&#xff0c;可以执行多语言语音识别、语音翻译和语言识别。 whisper有五种模型尺寸&#xff0c;提供速度和准确性的平衡&#xff0c;其中English-only模型…

软考初级程序员上午五单选(9)

1、在Windows中&#xff0c;用鼠标左键单击某应用程序窗口的最小化按钮后&#xff0c;该应用程序处于______的状态。 A&#xff0e;被强制关闭 B&#xff0e;不确定 C&#xff0e;被暂时挂起 D&#xff0e;在后台继续运行 2、将某ASCII字符采用偶校验编码(7位字符编码1位校验码…

毕业论文之转化为三线表格(wps)

目录 一、前言 1.修改之前的表格 2. 修改完成后&#xff08;三线表格式&#xff09; 二、操作步骤 一、前言 在论文里面的表格要求是三线表格式的时候&#xff0c;就需要我们去把这个表格修改成三线表格式。 1.修改之前的表格 2. 修改完成后&#xff08;三线表格式&…

Linux:centos:组账户管理 》》添加组,用户加入组(设置组密码),删除组,查询账户信息,查询登录用户信息

/etc/group # 组信息文件 /etc/gshadow # 组密码文件&#xff08;不常用&#xff09; groupadd &#xff08;属性&#xff09; 组名 # 新建组 groupdel &#xff08;属性&#xff09; 组名 # 删除组 gpasswd # 可以…

Gigabayte-Z87-DS3H i3 4130电脑 Hackintosh 黑苹果efi引导文件

原文来源于黑果魏叔官网&#xff0c;转载需注明出处。&#xff08;下载请直接百度黑果魏叔&#xff09; 硬件型号驱动情况 主板Gigabayte-Z87-DS3H 处理器英特尔酷睿i3 4130 Haswell已驱动 内存4x4GB DDR3/1600Mhz金士顿已驱动 硬盘SSD 480GB PNY CS900已驱动 显卡英特尔高…

【UE4】从零开始制作战斗机(上:准备模型、定义函数和变量)

资源连接&#xff1a;&#xff08;链接&#xff09; 步骤&#xff1a; 1. 下载完资源并解压&#xff0c;资源内容如下&#xff1a; 2. 将上图中所有的.fbx文件导入ue 使用默认的导入设置就行&#xff0c;直接点击导入所有 导入后内容如下&#xff1a; 将资源中的textures也导…

加速度传感器的量程估算

在测震动和噪声的场合&#xff0c;现有的加速度传感器&#xff0c;需要客户提供加速度值的大致区间。这个值该怎么计算呢&#xff1f;它几乎完全与被测信号的频率有关。因为所有的信号&#xff0c;按照频域展开的视角&#xff0c;都会简化为一个个正弦波。对于正弦波有这样的属…

Net跨平台UI框架Avalonia入门-资源和样式

Net跨平台UI框架Avalonia入门-资源和样式编写和使用 资源和样式编写和使用样式&#xff08;Styles&#xff09;和资源&#xff08;Resources&#xff09;样式&#xff08;Styles&#xff09;样式定义定义的位置:定义内容&#xff1a; 样式文件的定义和引用 资源&#xff08;Res…

浅谈PMO对组织战略的支持︱美团骑行事业部项目管理中心负责人边国华

美团骑行事业部项目管理中心负责人边国华先生受邀为由PMO评论主办的2023第十二届中国PMO大会演讲嘉宾&#xff0c;演讲议题&#xff1a;浅谈PMO对组织战略的支持。大会将于6月17-18日在北京举办&#xff0c;更多内容请浏览会议日程 议题内容简要&#xff1a; 战略是组织运行的…

【C++】C++中的多态

目录 一.多态的概念二.多态的定义及实现2.1虚函数2.2虚函数的重写虚函数重写的两个例外 2.3多态的构成条件2.4C11 override 和final2.5重载、重写、隐藏的对比 三.抽象类3.1概念3.2接口继承和实现继承 四.多态的原理4.1虚函数表4.2多态的原理(1)代码分析(2)清理解决方案 4.3动态…

AVUE样式、刷新、字典、清空搜索条件等操作

1、操作栏、表格样式的控制 2、下拉框字典的设置 3、日期格式的设置 const dateFormat function(row, value) { if (!value) return ; let format YYYY-mm-dd; let date new Date(value); const dataItem { Y: date.getFullYear().toString(), m: (date.ge…

LabVIEWCompactRIO 开发指南23 Web服务

LabVIEWCompactRIO 开发指南23 Web服务 LabVIEW8.6中引入的LabVIEWWeb服务提供了一种开放的标准方式&#xff0c;可通过Web与VI进行通信。考虑一个部署在分布式系统中的LabVIEW应用程序。LabVIEW提供了网络流等功能来建立通信&#xff0c;但许多开发人员需要一种方式&#xf…

实验10 超市订单管理系统综合实验

实验10 超市订单管理系统综合实验 应粉丝要求&#xff0c;本博主帮助实现基本效果&#xff01; 未避免产生版权问题&#xff0c;本项目博主不公开源码&#xff0c;如果您遇到相关问题可私聊博主&#xff01; 一、实验目的及任务 通过该实验&#xff0c;掌握利用SSM框架进行系…

ipa如何安装到iphone

这里以目前很火的奥普appuploader为例&#xff0c;先打开 appuploader&#xff0c;把 iPhone 用原装数据线连接&#xff0c;点击左侧的 appuploader一栏&#xff0c;会在右窗格中看到机器的相关信息&#xff0c;可以看到是否越狱一栏显示“是”。 接下来请点击左侧的“程序库”…

Android应用-开发框架设计

目录 1. &#x1f4c2; 简介 1.1 背景 1.2 专业术语 2. &#x1f531; 总体设计思想 2.1 分层&#xff1a;组件化设计框架 2.2 分类&#xff1a;应用开发架构图 3. ⚛️ 框架详细设计 3.1 组件化框架外形 3.2 业务模块化 3.3 代码编程框架 4. &#x1f4a0; 框架其他…

本地git仓库(gitea)与openssh-server的冲突(connection reset by ip port 22)

前提 之前在本地的windows电脑上安装了一个gitea供项目组成员使用。 期间为了在windows电脑上使用scp拷贝文件&#xff0c;离线安装过一个openssh。 冲突 发现无法pull/clone gitea上的仓库了&#xff0c;提示 connection reset by ip port 22 fatal: Could not read from r…

【ChatGPT】无需注册,无需科学上网,无需人工验证的速度超快的 ChatGPT

文章目录 一、ChatGPT介绍二、使用ChatGPT时经常遇到的一些问题三、一个让你呼吸顺畅的 ChatGPT 一、ChatGPT介绍 ChatGPT&#xff0c;全称聊天生成预训练转换器&#xff08;英语&#xff1a;Chat Generative Pre-trained Transformer&#xff09;&#xff0c;是OpenAI开发的人…

BGP路由选择实验

测试环境拓扑图 每一种规则测试完后记得恢复初始状态&#xff01;&#xff01; 各设备BGP Router_ID为loopback 0的地址。 AR1 配置 [V200R003C00] #sysname AR1 # interface GigabitEthernet0/0/0ip address 10.1.12.1 255.255.255.0 # interface LoopBack0ip address 1.1.…

vue大屏开发系列—使用echart开发省市地图数据,并点击省获取市地图数据

1. 本文在基础上进行改进&#xff0c;后端使用若依后端 IofTV-Screen: &#x1f525;一个基于 vue、datav、Echart 框架的物联网可视化&#xff08;大屏展示&#xff09;模板&#xff0c;提供数据动态刷新渲染、屏幕适应、数据滚动配置&#xff0c;内部图表自由替换、Mixins注入…