基于C#实现AC自动机算法

我要检查一篇文章中是否有某些敏感词,这其实就是多模式匹配的问题。当然你也可以用 KMP 算法求出,那么它的时间复杂度为 O(c*(m+n)),c:为模式串的个数。m:为模式串的长度,n:为正文的长度,那么这个复杂度就不再是线性了,我们学算法就是希望能把要解决的问题优化到极致,这不,AC 自动机就派上用场了。
其实 AC 自动机就是 Trie 树的一个活用,活用点就是灌输了 kmp 的思想,从而再次把时间复杂度优化到线性的 O(N),刚好我前面的文章已经说过了 Trie 树和 KMP,这里还是默认大家都懂。

一、构建 AC 自动机

同样我也用网上的经典例子,现有 say she shr he her 这样 5 个模式串,主串为 yasherhs,我要做的就是哪些模式串在主串中出现过?

1、构建 trie 树

如果看过我前面的文章,构建 trie 树还是很容易的。
image.png

2、失败指针

构建失败指针是 AC 自动机的核心所在,玩转了它也就玩转了 AC 自动机,失败指针非常类似于 KMP 中的 next 数组,也就是说,当我的主串在 trie 树中进行匹配的时候,如果当前节点不能再继续进行匹配,那么我们就会走到当前节点的 failNode 节点继续进行匹配,构建 failnode 节点也是很流程化的。
①:root 节点的子节点的 failnode 都是指向 root。
②:当走到在“she”中的”h“节点时,我们给它的 failnode 设置什么呢?此时就要走该节点(h)的父节点(s)的失败指针,一直回溯直到找到某个节点的孩子节点也是当初节点同样的字符(h),没有找到的话,其失败指针就指向 root。比如:h 节点的父节点为 s,s 的 failnode 节点为 root,走到 root 后继续寻找子节点为 h 的节点,恰好我们找到了,(假如还是没有找到,则继续走该节点的 failnode,嘿嘿,是不是很像一种回溯查找),此时就将 ”she"中的“h”节点的 fainode"指向"her"中的“h”节点,好,原理其实就是这样。(看看你的想法是不是跟图一样)
image.png
针对图中红线的”h,e“这两个节点,我们想起了什么呢?对”her“中的”e“来说,e 到 root 距离的 n 个字符恰好与”she“中的 e 向上的 n 个字符相等,我也非常类似于 kmp 中 next 函数,当字符失配时,next 数组中记录着下一次匹配时模式串的起始位置。

 #region Trie树节点/// <summary>/// Trie树节点/// </summary>public class TrieNode{/// <summary>/// 26个字符,也就是26叉树/// </summary>public TrieNode[] childNodes;/// <summary>/// 词频统计/// </summary>public int freq;/// <summary>/// 记录该节点的字符/// </summary>public char nodeChar;/// <summary>/// 失败指针/// </summary>public TrieNode faliNode;/// <summary>/// 插入记录时的编号id/// </summary>public HashSet<int> hashSet = new HashSet<int>();/// <summary>/// 初始化/// </summary>public TrieNode(){childNodes = new TrieNode[26];freq = 0;}}#endregion

刚才说到了 parent 和 current 两个节点,在给 trie 中的节点赋 failnode 的时候,如果采用深度优先的话还是很麻烦的,因为我要实时记录当前节点的父节点,相信写过树的朋友都清楚,除了深搜,我们还有广搜。

  /// <summary>/// 构建失败指针(这里我们采用BFS的做法)/// </summary>/// <param name="root"></param>public void BuildFailNodeBFS(ref TrieNode root){//根节点入队queue.Enqueue(root);while (queue.Count != 0){//出队var temp = queue.Dequeue();//失败节点TrieNode failNode = null;//26叉树for (int i = 0; i < 26; i++){//代码技巧:用BFS方式,从当前节点找其孩子节点,此时孩子节点//         的父亲正是当前节点,(避免了parent节点的存在)if (temp.childNodes[i] == null)continue;//如果当前是根节点,则根节点的失败指针指向rootif (temp == root){temp.childNodes[i].faliNode = root;}else{//获取出队节点的失败指针failNode = temp.faliNode;//沿着它父节点的失败指针走,一直要找到一个节点,直到它的儿子也包含该节点。while (failNode != null){//如果不为空,则在父亲失败节点中往子节点中深入。if (failNode.childNodes[i] != null){temp.childNodes[i].faliNode = failNode.childNodes[i];break;}//如果无法深入子节点,则退回到父亲失败节点并向root节点往根部延伸,直到null//(一个回溯再深入的过程,非常有意思)failNode = failNode.faliNode;}//等于null的话,指向root节点if (failNode == null)temp.childNodes[i].faliNode = root;}queue.Enqueue(temp.childNodes[i]);}}}

3、模式匹配

所有字符在匹配完后都必须要走 failnode 节点来结束自己的旅途,相当于一个回旋,这样做的目的防止包含节点被忽略掉。比如:我匹配到了"she",必然会匹配到该字符串的后缀”he",要想在程序中匹配到,则必须节点要走失败指针来结束自己的旅途。
image.png
从上图中我们可以清楚的看到“she”的匹配到字符"e"后,从 failnode 指针撤退,在撤退途中将其后缀字符“e”收入囊肿,这也就是为什么像 kmp 中的 next 函数。

/// <summary>
/// 根据指定的主串,检索是否存在模式串
/// </summary>
/// <param name="root"></param>
/// <param name="s"></param>
/// <returns></returns>
public void SearchAC(ref TrieNode root, string s, ref HashSet<int> hashSet)
{int freq = 0;TrieNode head = root;foreach (var c in s){//计算位置int index = c - 'a';//如果当前匹配的字符在trie树中无子节点并且不是root,则要走失败指针//回溯的去找它的当前节点的子节点while ((head.childNodes[index] == null) && (head != root))head = head.faliNode;//获取该叉树head = head.childNodes[index];//如果为空,直接给root,表示该字符已经走完毕了if (head == null)head = root;var temp = head;//在trie树中匹配到了字符,标记当前节点为已访问,并继续寻找该节点的失败节点。//直到root结束,相当于走了一个回旋。(注意:最后我们会出现一个freq=-1的失败指针链)while (temp != root && temp.freq != -1){freq += temp.freq;//将找到的id追加到集合中foreach (var item in temp.hashSet)hashSet.Add(item);temp.freq = -1;temp = temp.faliNode;}}
}

好了,到现在为止,我想大家也比较清楚了,最后上一个总的运行代码:

 using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Diagnostics;using System.Threading;using System.IO;namespace ConsoleApplication2{public class Program{public static void Main(){Trie trie = new Trie();trie.AddTrieNode("say", 1);trie.AddTrieNode("she", 2);trie.AddTrieNode("shr", 3);trie.AddTrieNode("her", 4);trie.AddTrieNode("he", 5);trie.BuildFailNodeBFS();string s = "yasherhs";var hashSet = trie.SearchAC(s);Console.WriteLine("在主串{0}中存在模式串的编号为:{1}", s, string.Join(",", hashSet));Console.Read();}}public class Trie{public TrieNode trieNode = new TrieNode();/// <summary>/// 用光搜的方法来构建失败指针/// </summary>public Queue<TrieNode> queue = new Queue<TrieNode>();#region Trie树节点/// <summary>/// Trie树节点/// </summary>public class TrieNode{/// <summary>/// 26个字符,也就是26叉树/// </summary>public TrieNode[] childNodes;/// <summary>/// 词频统计/// </summary>public int freq;/// <summary>/// 记录该节点的字符/// </summary>public char nodeChar;/// <summary>/// 失败指针/// </summary>public TrieNode faliNode;/// <summary>/// 插入记录时的编号id/// </summary>public HashSet<int> hashSet = new HashSet<int>();/// <summary>76             /// 初始化77             /// </summary>78             public TrieNode()79             {80                 childNodes = new TrieNode[26];81                 freq = 0;82             }83         }84         #endregion85 86         #region 插入操作87         /// <summary>88         /// 插入操作89         /// </summary>90         /// <param name="word"></param>91         /// <param name="id"></param>92         public void AddTrieNode(string word, int id)93         {94             AddTrieNode(ref trieNode, word, id);95         }96 97         /// <summary>98         /// 插入操作99         /// </summary>
100         /// <param name="root"></param>
101         /// <param name="s"></param>
102         public void AddTrieNode(ref TrieNode root, string word, int id)
103         {
104             if (word.Length == 0)
105                 return;
106 
107             //求字符地址,方便将该字符放入到26叉树中的哪一叉中
108             int k = word[0] - 'a';
109 
110             //如果该叉树为空,则初始化
111             if (root.childNodes[k] == null)
112             {
113                 root.childNodes[k] = new TrieNode();
114 
115                 //记录下字符
116                 root.childNodes[k].nodeChar = word[0];
117             }
118 
119             var nextWord = word.Substring(1);
120 
121             //说明是最后一个字符,统计该词出现的次数
122             if (nextWord.Length == 0)
123             {
124                 root.childNodes[k].freq++;
125                 root.childNodes[k].hashSet.Add(id);
126             }
127 
128             AddTrieNode(ref root.childNodes[k], nextWord, id);
129         }
130         #endregion
131 
132         #region 构建失败指针
133         /// <summary>
134         /// 构建失败指针(这里我们采用BFS的做法)
135         /// </summary>
136         public void BuildFailNodeBFS()
137         {
138             BuildFailNodeBFS(ref trieNode);
139         }
140 
141         /// <summary>
142         /// 构建失败指针(这里我们采用BFS的做法)
143         /// </summary>
144         /// <param name="root"></param>
145         public void BuildFailNodeBFS(ref TrieNode root)
146         {
147             //根节点入队
148             queue.Enqueue(root);
149 
150             while (queue.Count != 0)
151             {
152                 //出队
153                 var temp = queue.Dequeue();
154 
155                 //失败节点
156                 TrieNode failNode = null;
157 
158                 //26叉树
159                 for (int i = 0; i < 26; i++)
160                 {
161                     //代码技巧:用BFS方式,从当前节点找其孩子节点,此时孩子节点
162                     //         的父亲正是当前节点,(避免了parent节点的存在)
163                     if (temp.childNodes[i] == null)
164                         continue;
165 
166                     //如果当前是根节点,则根节点的失败指针指向root
167                     if (temp == root)
168                     {
169                         temp.childNodes[i].faliNode = root;
170                     }
171                     else
172                     {
173                         //获取出队节点的失败指针
174                         failNode = temp.faliNode;
175 
176                         //沿着它父节点的失败指针走,一直要找到一个节点,直到它的儿子也包含该节点。
177                         while (failNode != null)
178                         {
179                             //如果不为空,则在父亲失败节点中往子节点中深入。
180                             if (failNode.childNodes[i] != null)
181                             {
182                                 temp.childNodes[i].faliNode = failNode.childNodes[i];
183                                 break;
184                             }
185                             //如果无法深入子节点,则退回到父亲失败节点并向root节点往根部延伸,直到null
186                             //(一个回溯再深入的过程,非常有意思)
187                             failNode = failNode.faliNode;
188                         }
189 
190                         //等于null的话,指向root节点
191                         if (failNode == null)
192                             temp.childNodes[i].faliNode = root;
193                     }
194                     queue.Enqueue(temp.childNodes[i]);
195                 }
196             }
197         }
198         #endregion
199 
200         #region 检索操作
201         /// <summary>
202         /// 根据指定的主串,检索是否存在模式串
203         /// </summary>
204         /// <param name="s"></param>
205         /// <returns></returns>
206         public HashSet<int> SearchAC(string s)
207         {
208             HashSet<int> hash = new HashSet<int>();
209 
210             SearchAC(ref trieNode, s, ref hash);
211 
212             return hash;
213         }
214 
215         /// <summary>
216         /// 根据指定的主串,检索是否存在模式串
217         /// </summary>
218         /// <param name="root"></param>
219         /// <param name="s"></param>
220         /// <returns></returns>
221         public void SearchAC(ref TrieNode root, string s, ref HashSet<int> hashSet)
222         {
223             int freq = 0;
224 
225             TrieNode head = root;
226 
227             foreach (var c in s)
228             {
229                 //计算位置
230                 int index = c - 'a';
231 
232                 //如果当前匹配的字符在trie树中无子节点并且不是root,则要走失败指针
233                 //回溯的去找它的当前节点的子节点
234                 while ((head.childNodes[index] == null) && (head != root))
235                     head = head.faliNode;
236 
237                 //获取该叉树
238                 head = head.childNodes[index];
239 
240                 //如果为空,直接给root,表示该字符已经走完毕了
241                 if (head == null)
242                     head = root;
243 
244                 var temp = head;
245 
246                 //在trie树中匹配到了字符,标记当前节点为已访问,并继续寻找该节点的失败节点。
247                 //直到root结束,相当于走了一个回旋。(注意:最后我们会出现一个freq=-1的失败指针链)
248                 while (temp != root && temp.freq != -1)
249                 {
250                     freq += temp.freq;
251 
252                     //将找到的id追加到集合中
253                     foreach (var item in temp.hashSet)
254                         hashSet.Add(item);
255 
256                     temp.freq = -1;
257 
258                     temp = temp.faliNode;
259                 }
260             }
261         }
262         #endregion
263     }
264 }

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

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

相关文章

Autocad2020切换经典界面

Autocad2020切换经典界面 1.更改1.1设置另存为 1.更改 1.1设置另存为

迅为RK3568开发板学习之Linux驱动篇第十三期输入子系统

驱动视频全新升级&#xff0c;并持续更新~更全&#xff0c;思路更科学&#xff0c;入门更简单。 迅为基于iTOP-RK3568开发板进行讲解&#xff0c;本次更新内容为第十三期&#xff0c;主要讲解输入子系统&#xff0c;共计24 讲。 关注B站&#xff1a;北京迅为电子&#xff0c;在…

赛轮集团SAILUN方程式赛车轮胎震撼登场,开启新篇章

11月初&#xff0c;在厦门国际赛车场&#xff0c;SAILUN方程式赛车轮胎展现出令人瞩目的实力&#xff0c;成功完成了首次震撼亮相。这一引人注目的表现为未来的赛车轮胎技术发展打开了崭新的一页。 在这次首次亮相的测试中&#xff0c;职业车手巧妙操控着SAILUN方程式赛车轮胎&…

解决Vision Transformer在任意尺寸图像上微调的问题:使用timm库

解决Vision Transformer在任意尺寸图像上微调的问题&#xff1a;使用timm库 文章目录 一、ViT的微调问题的本质二、Positional Embedding如何处理1&#xff0c;绝对位置编码2&#xff0c;相对位置编码3&#xff0c;对位置编码进行插值 三、Patch Embedding Layer如何处理四、使…

气膜体育馆:低碳环保体育新潮流

在追求健康生活的今天&#xff0c;体育运动的重要性无法忽视。为了满足人民日益增长的体育需求&#xff0c;气膜体育馆应运而生&#xff0c;成为体育场馆领域的一次革命性创新。这种新型体育馆解决了传统体育场馆建设中面临的审批难、周期长、门槛高等问题&#xff0c;为我们的…

马蹄集oj赛(双周赛第十五次)

目录 小码哥的开心数字 淘金者 捡麦子 小码哥玩游戏 手机测试 自动浇花机 买月饼 未来战争 双人成行 魔法水晶球 ​编辑自驾游 文章压缩 银河贸易市场 小码哥的开心数字 子难度&#xff1a;青铜 0时间限制&#xff1a;1秒 巴占用内存&#xff1a;64M 小码哥有超能…

深入浅出 Linux 中的 ARM IOMMU SMMU I

Linux 系统下的 SMMU 介绍 在计算机系统架构中&#xff0c;与传统的用于 CPU 访问内存的管理的 MMU 类似&#xff0c;IOMMU (Input Output Memory Management Unit) 将来自系统 I/O 设备的 DMA 请求传递到系统互连之前&#xff0c;它会先转换请求的地址&#xff0c;并对系统 I…

海外IP代理:数据中心代理IP是什么?好用吗?

数据中心代理是代理IP中最常见的类型&#xff0c;也被称为机房IP。这些代理服务器为用户分配不属于 ISP&#xff08;互联网服务提供商&#xff09;而来自第三方云服务提供商的 IP 地址。数据中心代理的最大优势——它们允许在访问网络时完全匿名。 如果你正在寻找海外代理IP&am…

【JavaSE】-4-单层循环结构

回顾 运算符&#xff1a; 算术 --、逻辑 && & || |、比较 、三元 、赋值 int i 1; i; j i; //j2 i3 syso(--j"-----"i) //1 3 选择结构 if(){} if(){}else{} if(){}else if(){}else if(){}else{}//支持byte、short、int //支持char //支持枚举…

动态规划:2304. 网格中的最小路径代价

2304. 网格中的最小路径代价 给你一个下标从 0 开始的整数矩阵 grid &#xff0c;矩阵大小为 m x n &#xff0c;由从 0 到 m * n - 1 的不同整数组成。你可以在此矩阵中&#xff0c;从一个单元格移动到 下一行 的任何其他单元格。如果你位于单元格 (x, y) &#xff0c;且满足…

网络安全之渗透测试入门准备

渗透测试入门所需知识 操作系统基础&#xff1a;Windows&#xff0c;Linux 网络基础&#xff1a;基础协议与简单原理 编程语言&#xff1a;PHP&#xff0c;python web安全基础 渗透测试入门 渗透测试学习&#xff1a; 1.工具环境准备&#xff1a;①VMware安装及使用&#xff1b…

BUUCTF--[ACTF2020 新生赛]Include

目录 1、本题详解 2、延伸拓展 1、本题详解 访问题目链接 有一个tips的链接&#xff0c;我们点击 请求了file&#xff0c;内容是flag.php的内容&#xff1a;Can you find out the flag? 尝试请求一下index.php 并没有发现什么信息 flag.php也没发现什么 尝试爆破一下它的…

java游戏制作-飞翔的鸟游戏

一.准备工作 首先创建一个新的Java项目命名为“飞翔的鸟”&#xff0c;并在src中创建一个包命名为“com.qiku.bird"&#xff0c;在这个包内分别创建4个类命名为“Bird”、“BirdGame”、“Column”、“Ground”&#xff0c;并向需要的图片素材导入到包内。 二.代码呈现 …

Android线程优化——整体思路与方法

**在日常开发APP的过程中&#xff0c;难免需要使用第二方库和第三方库来帮助开发者快速实现一些功能&#xff0c;提高开发效率。但是&#xff0c;这些库也可能会给线程带来一定的压力&#xff0c;主要表现在以下几个方面&#xff1a; 线程数量增多&#xff1a;一些库可能会在后…

AIGC 是通向 AGI 的那条路吗?

AIGC 是通向 AGI 的那条路吗&#xff1f; 目录 一、背景知识 1.1、AGI&#xff08;人工通用智能&#xff09; 1.1.1、概念定义 1.1.2、通用人工智能特质 1.1.3、通用人工智能需要掌握能力 1.2、AIGC 二、AIGC 是通向 AGI 的那条路吗&#xff1f; 三、当前实现真正的 A…

【云原生】初识 Service Mesh

目录 一、什么是Service Mesh 二、微服务发展历程 2.1 微服务架构演进历史 2.1.1 单体架构 2.1.2 SOA阶段 2.1.3 微服务阶段 2.2 微服务治理中的问题 2.2.1 技术栈庞杂 2.2.2 版本升级碎片化 2.2.3 侵入性强 2.2.4 中间件多&#xff0c;学习成本高 2.2.5 服务治理功…

知虾数据软件:电商人必备知虾数据软件,轻松掌握市场趋势

在当今数字化时代&#xff0c;数据已经成为了企业决策的重要依据。对于电商行业来说&#xff0c;数据更是至关重要。如果你想在电商领域中脱颖而出&#xff0c;那么你需要一款强大的数据分析工具来帮助你更好地了解市场、分析竞争对手、优化运营策略。而知虾数据软件就是这样一…

【React-Router】导航传参

1. searchParams 传参 // /page/Login/index.js import { Link, useNavigate } from react-router-dom const Login () > {const navigate useNavigate()return <div>登录页<button onClick{() > navigate(/article?id91&namejk)}>searchParams 传参…

SpringBoot中使用注解的方式创建队列和交换机

SpringBoot中使用注解的方式创建队列和交换机 前言 最开始蘑菇博客在进行初始化配置的时候&#xff0c;需要手动的创建交换机&#xff0c;创建队列&#xff0c;然后绑定交换机&#xff0c;这个步骤是非常繁琐的&#xff0c;而且一不小心的话&#xff0c;还可能就出了错误&…