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 ...
随机推荐
- 学习GlusterFS(五)
一,分布式文件系统理论基础 1.1 分布式文件系统出现 计算机通过文件系统管理,存储数据,而现在数据信息爆炸的时代中人们可以获取的数据成指数倍的增长,单纯通过增加硬盘个数来扩展计算机文件系统的存储容量 ...
- 遇到问题之“postman报Unsupported Media Type: Content type 'text/plain;charset=UTF-8' not supported”
postman报Unsupported Media Type: Content type 'text/plain;charset=UTF-8' not supported postman之所以报Uns ...
- 获取Java数据库中结果集的每个字段名和个数
/** * 查询到多条数据, 封装到List<Map> */public List<Map<String, Object>> queryForMapList(Str ...
- cpu指令如何读写硬盘
我们提到cpu的主要作用之一就是控制设备之间的数据交互.这其中自然也包括了硬盘.系统的所有数据基本都在硬盘中,所以知道怎么读写硬盘,对程序来说非常重要,所以我们先来探索下传说中的pio模式. cpu要 ...
- C++ | 虚函数初探
虚函数 虚函数 是在基类中使用关键字 virtual 声明的函数.在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数. 我们想要的是在程序中任意点可以根据所调用的对象类型来选择调 ...
- Linux文件管理 | Liunx 常用命令
文件与目录基本操作 目录: 一.显示文件内容 cat 命令 more 命令 less 命令 head 命令 tail 命令 二.文件内容查询(grep) 三.文件查找命令 find 命令 locate ...
- Python中的numpy库介绍!
转自:https://blog.csdn.net/codedz/article/details/82869370 机器学习算法中大部分都是调用Numpy库来完成基础数值计算的.安装方法: pip3 i ...
- Vue.js快速介绍-超级马里奥像素艺术
原文出处:Quick Introduction to Vue.js - Super Mario Pixel Art ::代码我已经归纳在github上:[vue2-pixel-art]::::__查看 ...
- Web 开发中 Blob 与 FileAPI 使用简述
本文节选自 Awesome CheatSheet/DOM CheatSheet,主要是对 DOM 操作中常见的 Blob.File API 相关概念进行简要描述. Web 开发中 Blob 与 Fil ...
- 我的python学习记_03
数据类型 python中的数据类型包括:1.数字类型number:整型int(即整数) 浮点型float(小数形式,整数的话后面加".0") 布尔型(判断正确与否) 复数型(com ...