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

news2024/9/22 11:28:22

我要检查一篇文章中是否有某些敏感词,这其实就是多模式匹配的问题。当然你也可以用 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;

             //如果当前是根节点,则根节点的失败指针指向root
             if (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         #endregion
 85 
 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.coloradmin.cn/o/1237850.html

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

相关文章

设计模式篇---外观模式

文章目录 概念结构实例总结 概念 外观模式&#xff1a;为子系统中的一组接口提供一个统一的入口。外观模式定义了一个高层接口&#xff0c;这个接口使得这一子系统更加容易使用。 外观模式引入了一个新的外观类&#xff0c;它为多个业务类的调用提供了一个统一的入口。主要优点…

HSV映射到圆锥坐标系

def bgr2hsvcone(img):arr_hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV)h arr_hsv[..., 0] / 180. * 2s arr_hsv[..., 1] / 255.v arr_hsv[..., 2] / 255.x np.cos(h * np.pi) * s * vy np.sin(h * np.pi) * s * vreturn np.stack((x, y, v), axis-1)

Redis 的集群模式实现高可用

来源&#xff1a;Redis高可用&#xff1a;武林秘籍存在集群里&#xff0c;那稳了~ (qq.com) 1. 引言 前面我们已经聊过 Redis 的主从同步&#xff08;复制&#xff09;和哨兵机制&#xff0c;这期我们来聊 Redis 的集群模式。 但是在超大规模的互联网应用中&#xff0c;业务规…

七牛云产品使用介绍之CDN篇

上一篇介绍了七牛云的Kodo对象存储&#xff0c;并用Java SDK实现将本地文件上传到bucekt&#xff0c;接下来是对CDN产品的介绍 CDN&#xff08;内容分发网络&#xff09;&#xff1a;通过多级缓存实现对Kodo中的资源或者自己网站的资源的加速访问&#xff0c;让你的系统更快更强…

ProPresenter 7 for Mac:Mac电脑好用的文稿演示软件

ProPresenter 7是一款专业的多媒体演示软件&#xff0c;主要用于创建和展示高质量的教堂媒体演示、音乐歌词、幻灯片和视频等。它是一款功能强大且易于使用的软件&#xff0c;被广泛应用于教堂、会议、演唱会和其他场合。以下是ProPresenter 7的一些主要特点和功能&#xff1a;…

VSCode新建Vue项目

前言 Vue.js 是一款流行的 JavaScript 前端框架&#xff0c;它可以帮助开发者轻松构建高性能、可扩展的 Web 应用程序。而 VSCode 则是一款功能强大的开源代码编辑器&#xff0c;它提供了许多有用的工具和插件&#xff0c;可以大幅提高开发效率。 在本文中&#xff0c;我们将…

获取当前用户信息的几种方式

说明&#xff1a;在开发中&#xff0c;我们经常需要获取当前操作的用户信息&#xff0c;如创建用户、创建订单时&#xff0c;我们需要记录下创建人&#xff0c;本文介绍获取当前用户信息的三种方式。 方式一&#xff1a;使用ThreadLocal ThreadLocal本质上是一个Map&#xff…

悄悄上线:CSS @starting-style 新规则

最近 Chrome 117&#xff0c;CSS 又悄悄推出了一个新的的规则&#xff0c;叫做starting-style。从名称上来看&#xff0c;表示定义初始样式。那么&#xff0c;具体是做什么的&#xff1f;有什么用&#xff1f;一起了解一下吧 一、快速了解 starting-style 通常做一个动画效果…

基于springboot实现班级综合测评管理系统项目【项目源码+论文说明】

基于springboot实现班级综合测评管理系统演示 摘要 随着互联网技术的高速发展&#xff0c;人们生活的各方面都受到互联网技术的影响。现在人们可以通过互联网技术就能实现不出家门就可以通过网络进行系统管理&#xff0c;交易等&#xff0c;而且过程简单、快捷。同样的&#x…

HarmonyOS ArkTS语言,运行Hello World(一)

一、下载与安装DevEco Studio 在HarmonyOS应用开发学习之前&#xff0c;需要进行一些准备工作&#xff0c;首先需要完成开发工具DevEco Studio的下载与安装以及环境配置。 进入DevEco Studio下载官网&#xff0c;单击“立即下载”进入下载页面。 DevEco Studio提供了Windows…

这是基础校园二手交易框架

<!DOCTYPE html> <html lang"en"> <head> <meta charset"UTF-8"> <title>校园二手交易</title> <style> /* Reset stylesheet */ * { margin: 0; padding: 0; box-s…

HCIA-实验命令基础学习:

视频学习&#xff1a; 第一部分&#xff1a;基础学习。 19——子网掩码。 27——防火墙配置&#xff1a; 32——企业级路由器配置&#xff1a; 基础实验完成&#xff1a;&#xff08;完成以下目录对应的实验&#xff0c;第一部分基础实验就完成。&#xff09; 方法&#xff…

牛客 最小公配数 golang版实现

题目请参考: HJ108 求最小公倍数 题解: 在大的数的倍数里面去找最小的能整除另外一个数的数&#xff0c;就是最小公倍数&#xff0c;按照大的来找&#xff0c;循环次数能够降到很少&#xff0c;提升效率 golang实现: package mainimport ("fmt" )func main() {a : …

>Web 3.0顶级干货教学:浅析区块链与货币关系

Web 3.0顶级干货教学&#x1f525;&#xff1a;浅析区块链与货币关系 尊重原创&#xff0c;编写不易 &#xff0c;帮忙点赞关注一下~转载小伙伴请注明出处&#xff01;谢谢 1.0 数字交易 最早一笔数字化交易 是在www.PizzaHut.com 在 1994 年产生的&#xff0c;但是有趣的事情…

代餐粉产业分析:中国市场销售额增长至116.94亿元

近年来&#xff0c;随着人们生活节奏的加快和健康意识的增强&#xff0c;代餐粉市场规模逐渐壮大。在这个忙碌的时代&#xff0c;快捷、营养而又方便的代餐粉成为了许多人选择的首选。 随着健康理念的不断普及和推广&#xff0c;人们开始更加重视日常饮食的健康与营养。代餐粉作…

VUE excel表格导出

js代码 //下载模板 downloadExl() { // 标题 const tHeader [‘xxx’,xxx,xx名称,电枪xx,协议xx,snxx]; // key const filterVal [agentName, stationName, equName, channelNumber, manufacturer, sn, ]; // 值 const datas [ { agentName: 你好, stationName: 我们, e…

Doris 简介(一)

Apache Doris 由百度大数据部研发&#xff08;之前叫百度 Palo&#xff0c;2018 年贡献到 Apache 社区后&#xff0c;更名为 Doris &#xff09;&#xff0c;在百度内部&#xff0c;有超过 200 个产品线在使用&#xff0c;部署机器超过 1000 台&#xff0c;单一业务最大可达到上…

nohup 实现远程运行不关机操作

nohup 实现远程运行不宕机操作 python nohup 实现远程运行不宕机操作 - python教程网 远程运行最怕断电&#xff0c;训练了几个小时的数据说没就没&#xff0c;或者停止运行。 用nohup 记录代码的输出&#xff0c;还可以不受断电的影响。 方法 1. 用nohup 运行一个python文…

基于window10的远程桌面报错:要求的函数不受支持 的问题解决方法

基于window10的远程桌面报错&#xff1a;要求的函数不受支持 的问题解决方法 设置方法&#xff1a; 一、WINR 在框内输入gpedit.msc 二、依次打开 计算机配置----管理模板-----系统—凭据分配—加密数据库修正–改为以启用—易受攻击 第一步&#xff1a; 第二步&#xff1a;…

Springboot企业网站 毕业设计-附源码73192

摘 要 科技进步的飞速发展引起人们日常生活的巨大变化&#xff0c;电子信息技术的飞速发展使得电子信息技术的各个领域的应用水平得到普及和应用。信息时代的到来已成为不可阻挡的时尚潮流&#xff0c;人类发展的历史正进入一个新时代。在现实运用中&#xff0c;应用软件的工作…