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 ...
随机推荐
- Linux运维最常用150个命令
线上查询及帮助命令(2个) man 查看命令帮助,命令的词典,更复杂的还有info,但不常用. help 查看Linux内置命令的帮助,比如cd命令. 文件和目录操作命令(18个) ls 全拼list ...
- FPGA入门到精通系列1:数字电路基础知识
本文主要介绍数字电路基础知识,用最简洁的内容介绍最核心的知识. 1.数字电路是什么? 数字电路是利用电源电压的高电平和低电平分别表示1和0,进而实现信息的表达.模拟信号:随时间连续变化的信号.处理 ...
- TL431常用电路整理
熟悉电路制作的人大多对TL431并不陌生.由于TL431的动态抗阻的特性,其经常在电路设计当中被用于替代稳压二极管.不仅如此,TL431的开态响应速度快输出噪音低,并且价格低廉.因此受到电源工程师和初 ...
- 淘宝 rem 机制入门学习
一 移动设备尺寸多种多样,带来适配难度,有时甚至无从下手.1 移动设备上的Px 像素不等于设备的物理像素.iphone 6 作为开发标准设备不等于设备的物理像素.iPhone 5 物理宽度320iPh ...
- ES6-11学习笔记--Generator
基本使用 function后面加个*号 function* foo() { for (let i = 0; i < 3; i++) { yield i; // yield不能作为构造函数去使用, ...
- 在TypeScript项目中进行BDD测试
在TypeScript项目中进行BDD测试 什么是BDD? BDD(Behavior-Driven Design)是软件团队的一种工作方式,通过以下方式缩小业务人员和技术人员之间的差距: 鼓励跨角色协 ...
- Exception Handling Considered Harmful
异常处理被认为存在缺陷 Do, or do not. There is no try. - Yoda, The Empire Strikes Back (George Lucas) by Jason ...
- Android CheckBox的监听事件
1.在xml文件中定义CheckBox,一定要定义id <CheckBox android:id="@+id/beijing" android:layout_width=&q ...
- EMS恢复禁用邮箱
使用PowerShell命令恢复禁用邮箱 键入以下命令: [PS] C:\Windows\system32>Get-MailboxDatabase | Get-MailboxStatistics ...
- Java报错: A component required a bean of type 'com.sirifeng.testmybatis.mapper.BookMapper' that could not be found.
在学习Spring-boot-mybatis时,报错A component required a bean of type 'com.sirifeng.testmybatis.mapper.BookM ...