简介

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的更多相关文章

  1. hdu 4117 GRE Words (ac自动机 线段树 dp)

    参考:http://blog.csdn.net/no__stop/article/details/12287843 此题利用了ac自动机fail树的性质,fail指针建立为树,表示父节点是孩子节点的后 ...

  2. hdu 4117 -- GRE Words (AC自动机+线段树)

    题目链接 problem Recently George is preparing for the Graduate Record Examinations (GRE for short). Obvi ...

  3. 【BZOJ2434】阿狸的打字机(AC自动机,树状数组)

    [BZOJ2434]阿狸的打字机(AC自动机,树状数组) 先写个暴力: 每次打印出字符串后,就插入到\(Trie\)树中 搞完后直接搭\(AC\)自动机 看一看匹配是怎么样的: 每次沿着\(AC\)自 ...

  4. 【BZOJ2434】【NOI2011】阿狸的打字机(AC自动机,树状数组)

    [BZOJ2434]阿狸的打字机(AC自动机,树状数组) 先写个暴力: 每次打印出字符串后,就插入到\(Trie\)树中 搞完后直接搭\(AC\)自动机 看一看匹配是怎么样的: 每次沿着\(AC\)自 ...

  5. 【集训第二天·翻水的老师】--ac自动机+splay树

    今天是第二天集训.(其实已经是第三天了,只是昨天并没有机会来写总结,现在补上) 上午大家心情都很愉快,因为老师讲了splay树和ac自动机. 但到了下午,我们的教练竟然跑出去耍了(excuse me? ...

  6. AC自动机——多个kmp匹配

    (并不能自动AC) 介绍: Aho-Corasick automaton,最经典的处理多个模式串的匹配问题. 是kmp和字典树的结合. 精髓与灵魂: ①利用trie处理多个模式串 ②引入fail指针. ...

  7. AC自动机fail树小结

    建议大家学过AC自动机之后再来看这篇小结 fail树就是讲fail指针看做一条边连成的树形结构 fail指针在AC自动机中的含义是指以x为结尾的后缀在其他模式串中所能匹配的最长前缀的长度 所以在模式串 ...

  8. BZOJ 2434: [Noi2011]阿狸的打字机 [AC自动机 Fail树 树状数组 DFS序]

    2434: [Noi2011]阿狸的打字机 Time Limit: 10 Sec  Memory Limit: 256 MBSubmit: 2545  Solved: 1419[Submit][Sta ...

  9. BZOJ 3172: [Tjoi2013]单词 [AC自动机 Fail树]

    3172: [Tjoi2013]单词 Time Limit: 10 Sec  Memory Limit: 512 MBSubmit: 3198  Solved: 1532[Submit][Status ...

随机推荐

  1. 学习Apache(四)

    介绍 Apache HTTP 服务器被设计为一个功能强大,并且灵活的 web 服务器, 可以在很多平台与环境中工作.不同平台和不同的环境往往需要不同 的特性,或可能以不同的方式实现相同的特性最有效率. ...

  2. 记录md的偏好设置

  3. HTML5 版的flappy bird

    Flappy Bird这款简单的小游戏累计下载量已经超过5000万次,每天收入至少5万美元.然而,2月10日其开发者Dong Nguyen却将Flappy Bird从苹果App Store和Googl ...

  4. html5知识点补充—footer元素的使用

    使用footer元素创建脚注 顾名思义,footer元素通常位于页面的底部.尽管footer通常位于某个区域或者页面的底部,但并非总是如此.footer元素旨在包含作者.网站所有者.版权数据.网站规章 ...

  5. 如何监控微信小程序HTTP请求错误

    摘要: Fundebug的微信小程序错误监控插件更新至0.5.0,支持监控HTTP请求错误. 接入插件 接入Fundebug的错误监控插件非常简单,只需要下载fundebug.0.5.0.min.js ...

  6. python-输入列表,求列表元素和(eval输入应用)

    在一行中输入列表,输出列表元素的和. 输入格式: 一行中输入列表. 输出格式: 在一行中输出列表元素的和. 输入样例: [3,8,-5] 输出样例: 6 代码: a = eval(input()) t ...

  7. rabitmq 登录报错:User can only log in via localhost

    安装教程参考:https://blog.csdn.net/qq_43672652/article/details/107349063 修改了配置文件仍然报错,无法登录.解决办法:新建一个用户登录: 查 ...

  8. JavaScript学习总结6-apply

    JS中的apply方法可以控制this指向 任何JS支持的类型都可以转化为JSON JS对象是键值对型的,JSON是字符串型的 原型对象:__proto__ JS万物皆对象,ES6开始提供了对后端开发 ...

  9. 新手入门C语言第九章:C函数

    C 函数 函数是一组一起执行一个任务的语句.每个 C 程序都至少有一个函数,即主函数 main() ,所有简单的程序都可以定义其他额外的函数.您可以把代码划分到不同的函数中.如何划分代码到不同的函数中 ...

  10. 新的 css 子选择器

    1. html 结构 <!DOCTYPE html> <html lang="en"> <head> <meta charset=&quo ...