AC自动机:Tire树+KMP
简介
AC自动机是一个多模式匹配算法,在模式匹配领域被广泛应用,举一个经典的例子,违禁词查找并替换为***。AC自动机其实是Trie树和KMP 算法的结合,首先将多模式串建立一个Tire树,然后结合KMP算法前缀与后缀匹配可以减少不必要比较的思想达到高效找到字符串中出现的匹配串。 如果不知道什么是Tire树,可以先查看:图解Tire树+代码实现 如果不知道KMP算法,可以先查看:图解KMP字符串匹配算法
工作过程
首先看一下AC自动机的结构,从造型上看,跟我们之前讲Tire树几乎一样,但是多了红色线条(这里因为画完太乱,没有画完),这个红色线条我们称为失败指针。其匹配规则与KMP一致,后缀和前缀的匹配,不一样的是,KMP是同一个模式串的前缀和后缀进行匹配,而这里是当前模式串的后缀,与另一个模式串的前缀进行匹配。如果能够匹配上,因为这两个模式串的前缀一定不同(相同的前缀已经聚合),将当前已匹配的后缀拿出来,比如abo,后缀为o,bo,abo,这时候我们再找另一个模式串的最长前缀与当前后缀匹配上(对应kmp中的最长前缀后缀子串),这时候我们可以找到out的o,则about中的o节点的失败指针指向out的o节点,这么做的意义就是主串可以一直往后比较,不用往前回溯(比如ab,之前匹配过能匹配上,但是到o是失败了,其他匹配串不可能出现ab前缀,所以不必再匹配,一定失败)。 构建过程:建立一棵Tire树,结尾节点需要标志当前模式串的长度,构建失败指针。 查找过程:从根节点出发,查找当前节点的孩子节点是否有与当前字符匹配的字符,匹配则判断是否为尾节点,是则匹配成功,记录。不是尾节点继续匹配。如果孩子节点没有与字符匹配的,则直接转到失败指针继续操作。
数据结构
一个value记录当前节点的值,childNode记录当前节点的子节点(假设仅出现26个小写字母,空间存在浪费,可使用hash表,有序二分,跳表进行优化),isTail标志当前节点是否为尾节点,failNode表示失败指针,即当前节点的孩子节点与当前字符均不匹配的时候,转到哪个节点接续进行匹配,tailLength,记录模式串的长度,方便快速拿出模式串的值(根据长度以及匹配的index,从主串中拿)。
public static class Node{ //当前节点值 private char value; //当前节点的孩子节点 private Node[] childNode; //标志当前节点是否是某单词结尾 private boolean isTail; //失败指针 private Node failNode; //匹配串长度,当isTail==true时,表示从root当当前位置是一个完整的匹配串,记录这个匹配串的长度,便于之后快速找到匹配串 private Integer tailLength; public Node(char value) { this.value = value; } }
初始化
初始化一棵仅存在root的根节点,root的失败指针以及长度均为null。
Node root; public void init() { root = new Node('\0'); root.childNode = new Node[26]; }
构建字典树
这个过程之前Tire树中已经讲过,不再赘述,唯一的区别是需要在结尾节点上标志当前模式串的长度。
public void insertStr(char[] chars) { //首先判断首字符是否已经在字典树中,然后判断第二字符,依次往下进行判断,找到第一个不存在的字符进行插入孩节点 Node p = root; //表明当前处理到了第几个字符 int chIndex = 0; while (chIndex < chars.length) { while (chIndex < chars.length && null != p) { Node[] children = p.childNode; boolean find = false; for (Node child : children) { if (null == child) {continue;} if (child.value == chars[chIndex]) { //当前字符已经存在,不需要再进行存储 //从当前节点出发,存储下一个字符 p = child; ++ chIndex; find = true; break; } } if (Boolean.TRUE.equals(find)) { //在孩子中找到了 不用再次存储 break; } //如果把孩子节点都找遍了,还没有找到这个字符,直接将这个字符加入当前节点的孩子节点 Node node = new Node(chars[chIndex]); node.childNode = new Node[26]; children[chars[chIndex] - 'a'] = node; p = node; ++ chIndex; } } //字符串中字符全部进入tire树中后,将最后一个字符所在节点标志为结尾节点 p.isTail = true; p.tailLength = chars.length; }
构建失败指针
从根节点开始层序遍历树结构,构建失败指针。一个节点的子节点的失败指针可以根据当前节点的失败指针得到,因为我们是用后缀去与前缀匹配,所以如果我们采用层序遍历,与当前后缀的前缀一定在上层,已经匹配出来了。那么当前节点的子节点的失败指针则可以根据当前节点的失败指针,查找失败指针指向的节点的子节点是否有与当前节点的子节点相等的,相等则这个子节点的失败指针直接指向,不相等则继续找,找不到直接指向root。根据上面的图,我们来举一个例子,我们已经找到about中o节点(o1)的失败指针是out中的o节点(o2),接下来我们怎么找u(u1)的失败指针呢?首先根据o1的失败指针我们找到了o2,o2的子节点为u(u2),恰好与我们u1的值相等,此时我们就可以将u1的失败指针指向u2。以此类推,如果访问到最后为空(root的失败指针为空),则直接将失败指针指向root。
public void madeFailNext() { //层序遍历,为了保证求解这个节点失败指针的时候,它的父节点的失败指针以及失败指针的失败指针。。。。已经求得,可以完全根据这个找 Deque<Node> nodes = new LinkedList<>(); nodes.add(root); while (!nodes.isEmpty()) { Node current = nodes.poll(); Node[] children = current.childNode; for (Node child : children) { if (null == child) { continue; } Node failNode = current.failNode; while (null != failNode) { //找到当前节点的失败指针,查看失败指针子节点是否有== Node[] failChildren = failNode.childNode; Node node = failChildren[child.value - 'a']; if (null == node) { //找当前指针的下一个指针 failNode = failNode.failNode; continue; } //已经找到匹配的 //将失败指针指向node child.failNode = node; break; } //如果找完还没有找到,指向root if (null == failNode) { child.failNode = root; } nodes.add(child); } } }
匹配
从首字符,字典树从root节点开始进行匹配,如果字符与节点值匹配,则判断是否为尾字符,如果是匹配上一个违禁词,记录下来,如果不匹配则转移到失败指针继续进行匹配。
/** * 匹配出str中所有出现的关键词 * @param str * @return */ public List<String> match(String str) { //遍历当前子串串,从根节点出发,如果匹配就一直往下进行匹配,同时需要看匹配的节点是否为结尾节点,如果是,匹配上一个 //如果不匹配则通过失败指针转移到下一个节点 this.dfs(root, 0, str); return machStr; } //abcdeasdabcebcd List<String> machStr = new ArrayList<>(); private void dfs(Node node, int chIndex, String chars) { if (chIndex >= chars.length()) { return; } //从将当前字符与当前node的孩子节点进行匹配,如果当前字符与node的孩子节点.value匹配,判断当前字符是否为尾节点,是,则记录,匹配到了一个 //继续匹配(子节点,与下一个元素进行匹配) //如果不匹配,则转到失败指针 Node[] children = node.childNode; Node child = children[chars.charAt(chIndex) - 'a']; if (null == child) { //不匹配,转到失败指针 //如果当前node==root,从root匹配,root的失败指针是null if (node == root) { dfs(root, ++ chIndex, chars); } else { dfs(node.failNode, chIndex, chars); } } else { //匹配到了 if (child.isTail) { //并且是结尾节点,取从child.value到child.tailLength的字符 machStr.add(chars.substring(chIndex - child.tailLength + 1, chIndex + 1)); } dfs(child, ++ chIndex, chars); } }
执行结果
public static void main(String[] args) { ACAutomaton acAutomaton = new ACAutomaton(); //初始化一个仅有根节点的字典树 acAutomaton.init(); //构建Tire树 acAutomaton.insertStr("out".toCharArray()); acAutomaton.insertStr("about".toCharArray()); acAutomaton.insertStr("act".toCharArray()); //构建失败指针 acAutomaton.madeFailNext(); System.out.println("ces"); //匹配 List<String> result = acAutomaton.match("abcdeasactdaboutcebcd"); }
AC自动机:Tire树+KMP的更多相关文章
- hdu 4117 GRE Words (ac自动机 线段树 dp)
参考:http://blog.csdn.net/no__stop/article/details/12287843 此题利用了ac自动机fail树的性质,fail指针建立为树,表示父节点是孩子节点的后 ...
- hdu 4117 -- GRE Words (AC自动机+线段树)
题目链接 problem Recently George is preparing for the Graduate Record Examinations (GRE for short). Obvi ...
- 【BZOJ2434】阿狸的打字机(AC自动机,树状数组)
[BZOJ2434]阿狸的打字机(AC自动机,树状数组) 先写个暴力: 每次打印出字符串后,就插入到\(Trie\)树中 搞完后直接搭\(AC\)自动机 看一看匹配是怎么样的: 每次沿着\(AC\)自 ...
- 【BZOJ2434】【NOI2011】阿狸的打字机(AC自动机,树状数组)
[BZOJ2434]阿狸的打字机(AC自动机,树状数组) 先写个暴力: 每次打印出字符串后,就插入到\(Trie\)树中 搞完后直接搭\(AC\)自动机 看一看匹配是怎么样的: 每次沿着\(AC\)自 ...
- 【集训第二天·翻水的老师】--ac自动机+splay树
今天是第二天集训.(其实已经是第三天了,只是昨天并没有机会来写总结,现在补上) 上午大家心情都很愉快,因为老师讲了splay树和ac自动机. 但到了下午,我们的教练竟然跑出去耍了(excuse me? ...
- AC自动机——多个kmp匹配
(并不能自动AC) 介绍: Aho-Corasick automaton,最经典的处理多个模式串的匹配问题. 是kmp和字典树的结合. 精髓与灵魂: ①利用trie处理多个模式串 ②引入fail指针. ...
- AC自动机fail树小结
建议大家学过AC自动机之后再来看这篇小结 fail树就是讲fail指针看做一条边连成的树形结构 fail指针在AC自动机中的含义是指以x为结尾的后缀在其他模式串中所能匹配的最长前缀的长度 所以在模式串 ...
- BZOJ 2434: [Noi2011]阿狸的打字机 [AC自动机 Fail树 树状数组 DFS序]
2434: [Noi2011]阿狸的打字机 Time Limit: 10 Sec Memory Limit: 256 MBSubmit: 2545 Solved: 1419[Submit][Sta ...
- BZOJ 3172: [Tjoi2013]单词 [AC自动机 Fail树]
3172: [Tjoi2013]单词 Time Limit: 10 Sec Memory Limit: 512 MBSubmit: 3198 Solved: 1532[Submit][Status ...
随机推荐
- GC和GC Tuning
GC和GC Tuning GC的基础知识 什么是垃圾 C语言申请内存:malloc free C++: new delete c/C++ 手动回收内存 Java: new ? 自动内存回收,编程上简单 ...
- 5_终值定理和稳态误差_Final Value Theorem & Steady State Error
- Vue-router实现单页面应用在没有登录情况下,自动跳转到登录页面
这是我做前端一来的第一篇文章,都不知道该怎么开始了.那就直接奔主题吧.先讲讲这个功能的实现场景吧,我们小组使用vue全家桶实现了一个单页面应用,最初就考虑对登录状态做限制.比如登录后不能后退到登录页面 ...
- CCF201812-1小明上学
题目背景 小明是汉东省政法大学附属中学的一名学生,他每天都要骑自行车往返于家和学校.为了能尽可能充足地睡眠,他希望能够预计自己上学所需要的时间.他上学需要经过数段道路,相邻两段道路之间设有至多一盏红绿 ...
- 利用es6解构赋值快速提取JSON数据;
直接上代码 { let JSONData = { title:'abc', test:[ { nums:5, name:'jobs' }, { nums:11, name:'bill' } ] } l ...
- python中其他数据类型内置方法
补充字符串数据类型内置方法 1.移除字符串首尾的指定字符可以选择方向1: s1 = '$$$jason$$$' print(s1.strip('$')) # jason print(s1.lstrip ...
- 启动分区查找可以通过 fdisk -l命令
这里有两个硬盘,一个硬盘有两个分区,sda1 的boot列 带*表示是启动分区,否则为空
- 基于STM32单片机的简单红外循迹的实现
初步接触STM32,采用两路红外传感器实现小车循迹,稍显简略,如有不好的地方,欢迎大家指点改正
- Zabbix6 网络发现
Zabbix6 网络发现 功能 快速发现并添加主机 简单的管理 随着环境的改变而快速搭建系统 发现配置依据 IP地址段 基于服务(FTP.SSH.Web.POP3.IMAP.TCP-)的 从Zabbi ...
- 学习day44
初步学完html的知识