那些经典算法:AC自动机

第一次看到这个名字的时候觉得非常高级,深入学习就发现,AC就是一种多模式字符串匹配算法。前面介绍的BF算法,RK算法,BM算法,KMP算法都属于单模式匹配算法,而Trie树是多模式匹配算法,多模式匹配算法就是在一个主串中查找多个模式串,举个最常用的例子,比如我们在论坛发表评论或发帖的时候,一般论坛后台会检测我们发的内容是否有敏感词,如果有敏感词要么是用***替换,要么是不让你发送,我们评论是通常是一段话,这些敏感词可能成千上万,如果用每个敏感词都在评论的内容中查找,效率会非常低,AC自动机中,主串会与所有的模式串同时匹配,这时候就可以利用AC自动机这种多模式匹配算法来完成高效的匹配,

一 AC自动机算法原理

AC自动机算法是构造一个Trie树,然后再添加额外的失配指针。这些额外的适配指针准许在查找字符串失败的时候进行回退(例如在Trie树种查找单词bef失败后,但是在Trie树种存中bea这个单词,失配指针会指向前缀be),转向某些前缀分支,免于重复匹配前缀,提高算法效率。
常见于IDS软件或病毒检测软件中病毒特征字符串,可以构建AC自动机,在这种情况下,算法的时间复杂度为输入字符串的长度和匹配数量之和。

假设现有模式字符串集合:{abd,abdk, abchijn, chnit, ijabdf, ijaij} 构建AC自动机如下:


AC自动机图来自互联网

说明:

  • 根节点不存在任何字符,根节点的fail指针为null。
  • 虚线表示该节点的fail指针,所有模式串中字符串最后一个字符节点外面用红圈表示,说明是匹配上的字符串。
  • 每个节点都有fail指针,为了方便图中未画出根节点的fail虚线。
    每个节点的fail指针表示从根节点到该节点所组成字符序列中所有后缀和目标的模式串集合中所有前缀 两者中最长的公共部分。
    举例:
    图中,从根节点到目标字符串“ijabdf”中d组成字符序列“ijabd”的所有后缀在整个模式串中:
    {abd,abdk, abchijn, chnit, ijabdf, ijaij}的所有前缀中,最长的公共部分就是abd,所以“ijabdf”中d的fail指针就是指向abd中的d。
    https://www.cnblogs.com/nullzx/p/7499397.html

二 AC自动机运行过程

1)当前指针curr指向AC自动机的根节点:curr=root。
2)从文本串中读取(下)一个字符。
3)从当前节点的所有孩子节点中寻找与该字符匹配的节点:

  • 如果成功:判断当前节点以及当前节点fail指向的节点是否表示字符串结束,则将匹配的字符串(从根节点到结束节点)保存。curr指向孩子节点,继续执行步骤2。
  • 如果失败执行步骤4

4)若fail == null,则说明没有任何子串为输入字符串的前缀,这时设置curr = root,执行步骤2.
若fail != null,则将curr指向 fail节点,指向步骤3。
理解起来比较复杂,找网上的一个例子,假设文本串text = “abchnijabdfk”。
查找过程如下:


AC自动机执行过程

说明如下:
1)按照字符串顺序依次遍历到:a-->b-->c-->h ,这时候发现文本串中下一个节点n和Trie树中下一个节点i不匹配,且h的fail指针非空,跳转到Trie树中ch位置。
注意c-->h的时候判断h不为结束节点,且c的fail指针也不是结束节点。
2)再接着遍历n-->i,发现i节点在Trie树中的下一个节点找不到j,且有fail指针,则继续遍历,
遍历到d的时候要注意,d的下一个匹配节点f是结束字符,所以得到匹配字符串:ijabdf,且d的fail节点也是d,且也是结束字符,所以得到匹配字符串abd,不过不是失败的匹配,所以curr不跳转。

三 AC自动机的构造过程

先将目标字符串插入到Trie树种,然后通过广度有限遍历为每个节点的所有孩子节点找到正确的fail指针。
具体步骤如下:
1)将根节点的所有孩子节点的fail指针指向根节点,然后将根节点的所有孩子节点依次入队列。
2)若队列不为空:
2.1)出列一个字符,将出列的节点记为curr,failTo表示curr的
fail指针,即failTo = curr.fail 。
2.2) 判断curr.child[i] == failTo.child[i]是不是成立:
成立:curr.child[i].fail = failTo.child[i]
因为当前字符串的后缀和Tire树的前缀最长部分是到fail,
且子字符和failTo的下一个字符相同,则fail指针就是
failTo.child[i]。
不成立: 判断failTo是不是为null是否成立:
成立: curr.child[i].fail = root = null。
不成立: failTo = failTo.fail 继续2.2
curr.child[i]入列,再次执行步骤2)。
3)队列为空结束。

四 实例理解

每个结点的fail指向的解决顺序是按照广度有限遍历的顺序完成的,或者说层序遍历的顺序进行,我们根据父结点的fail指针来求当前节点的fail指针。


AC自动机查找演示A

理解fail指针的含义:表示从根节点到该节点所组成字符序列的所有后缀和 整个模式字符串集合即整个Trie树中 所有前缀 两者中的最长公共部分。

上图为例,我们要解决y节点的fail指针问题,已经知道y节点的父节点x1的fail是指向x2的,根据fail指针的定义,我们知道红色椭圆中的字符串序列肯定相等,而且是最长的公共部分。依据y.fail的含义,如果x2的某个孩子节点和节点y表示的表示的字符相等,y的fail就指向它。
如果x2的孩子节点中不存在节点y表示的字符。由于x2.fail指向x3,根据x2.fail的含义,我们知道绿色框中的字符序列是相同的。显然如果x3的某个孩子和节点y表示字符相等,则y.fail就指向它。

如果x3的孩子节点不存在节点y表示的字符,我们重复这个步骤,直到xi的fail节点指向null,说明我们达到顶层,只要y.fail= root就可以了。
构造过程就是知道当前节点的最长公共前缀的情况下,去确定孩子节点的最长公共前缀。

4.1 确定图中h节点fail指向的过程

下图中,每个节点都有fail虚线,指向根节点的虚线没画出,求图中c的孩子节点h的fail指向:


原图

H的Fail确定图

原图中,深蓝色的框出来的是已经确定fail指针的,求红色框中h节点的fail指针。
这时候,我们看下h的父亲节点c的fail指针指向,为ch中的c(这表示abc字符串的所有后缀bc和c和Trie树的所有前缀中最长公共部分为c),且这个c节点的孩子节点中有字符为h的字符,所以图中红色框中框出的h节点的fail指针指向 ch字符串中的h。

4.2 确定图中i.fail指向

原图2
I的Fail指针

求红色框中i的fail指针指向,上图中,我们可以看到i的父亲节点h的指向为ch中的h,(也就是说我们的目标字符串结合中所有前缀和字符序列abch的所有后缀在Trie树中最长前缀为ch。)我们比较i节点和ch中的h的所有子节点,发现h只有一个n的子节点,所以没办法匹配,那就继续找ch中h的fail指针,图中没画出,那么就是它的fail指针就是root,然后去看root所有子节点中有没有和i相等的,发现最右边的i是和我们要找的i相等的,所以我们就把i的fail指针指向i,如后面的图。

五 代码实现

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map.Entry;
 
public class AhoCorasickAutomation {
    /*本示例中的AC自动机只处理英文类型的字符串,所以数组的长度是128*/
    private static final int ASCII = 128;
     
    /*AC自动机的根结点,根结点不存储任何字符信息*/
    private Node root;
     
    /*待查找的目标字符串集合*/
    private List<String> target;
     
    /*表示在文本字符串中查找的结果,key表示目标字符串, value表示目标字符串在文本串出现的位置*/
    private HashMap<String, List<Integer>> result;
     
    /*内部静态类,用于表示AC自动机的每个结点,在每个结点中我们并没有存储该结点对应的字符*/
    private static class Node{
         
        /*如果该结点是一个终点,即,从根结点到此结点表示了一个目标字符串,则str != null, 且str就表示该字符串*/
        String str;
         
        /*ASCII == 128, 所以这里相当于128叉树*/
        Node[] table = new Node[ASCII];
         
        /*当前结点的孩子结点不能匹配文本串中的某个字符时,下一个应该查找的结点*/
        Node fail;
         
        public boolean isWord(){
            return str != null;
        }
         
    }
     
    /*target表示待查找的目标字符串集合*/
    public AhoCorasickAutomation(List<String> target){
        root = new Node();
        this.target = target;
        buildTrieTree();
        build_AC_FromTrie();
    }
     
    /*由目标字符串构建Trie树*/
    private void buildTrieTree(){
        for(String targetStr : target){
            Node curr = root;
            for(int i = 0; i < targetStr.length(); i++){
                char ch = targetStr.charAt(i);
                if(curr.table[ch] == null){
                    curr.table[ch] = new Node();
                }
                curr = curr.table[ch];
            }
            /*将每个目标字符串的最后一个字符对应的结点变成终点*/
            curr.str = targetStr;
        }
    }
     
    /*由Trie树构建AC自动机,本质是一个自动机,相当于构建KMP算法的next数组*/
    private void build_AC_FromTrie(){
        /*广度优先遍历所使用的队列*/
        LinkedList<Node> queue = new LinkedList<Node>();
         
        /*单独处理根结点的所有孩子结点*/
        for(Node x : root.table){
            if(x != null){
                /*根结点的所有孩子结点的fail都指向根结点*/
                x.fail = root;
                queue.addLast(x);/*所有根结点的孩子结点入列*/
            }
        }
         
        while(!queue.isEmpty()){
            /*确定出列结点的所有孩子结点的fail的指向*/
            Node p = queue.removeFirst();
            for(int i = 0; i < p.table.length; i++){
                if(p.table[i] != null){
                    /*孩子结点入列*/
                    queue.addLast(p.table[i]);
                    /*从p.fail开始找起*/
                    Node failTo = p.fail;
                    while(true){
                        /*说明找到了根结点还没有找到*/
                        if(failTo == null){
                            p.table[i].fail = root;
                            break;
                        }
                         
                        /*说明有公共前缀*/
                        if(failTo.table[i] != null){
                            p.table[i].fail = failTo.table[i];
                            break;
                        }else{/*继续向上寻找*/
                            failTo = failTo.fail;
                        }
                    }
                }
            }
        }
    }
     
    /*在文本串中查找所有的目标字符串*/
    public HashMap<String, List<Integer>> find(String text){
        /*创建一个表示存储结果的对象*/
        result = new HashMap<String, List<Integer>>();
        for(String s : target){
            result.put(s, new LinkedList<Integer>());
        }
         
        Node curr = root;
        int i = 0;
        while(i < text.length()){
            /*文本串中的字符*/
            char ch = text.charAt(i);
             
            /*文本串中的字符和AC自动机中的字符进行比较*/
            if(curr.table[ch] != null){
                /*若相等,自动机进入下一状态*/
                curr = curr.table[ch];
                 
                if(curr.isWord()){
                    result.get(curr.str).add(i - curr.str.length()+1);
                }
                 
                /*这里很容易被忽视,因为一个目标串的中间某部分字符串可能正好包含另一个目标字符串,
                 * 即使当前结点不表示一个目标字符串的终点,但到当前结点为止可能恰好包含了一个字符串*/
                if(curr.fail != null && curr.fail.isWord()){
                    result.get(curr.fail.str).add(i - curr.fail.str.length()+1);
                }
                 
                /*索引自增,指向下一个文本串中的字符*/
                i++;
            }else{
                /*若不等,找到下一个应该比较的状态*/
                curr = curr.fail;
                 
                /*到根结点还未找到,说明文本串中以ch作为结束的字符片段不是任何目标字符串的前缀,
                 * 状态机重置,比较下一个字符*/
                if(curr == null){
                    curr = root;
                    i++;
                }
            }
        }
        return result;
    }
     
     
    public static void main(String[] args){
        List<String> target = new ArrayList<String>();
        target.add("abcdef");
        target.add("abhab");
        target.add("bcd");
        target.add("cde");
        target.add("cdfkcdf");
         
        String text = "bcabcdebcedfabcdefababkabhabk";
         
        AhoCorasickAutomation aca = new AhoCorasickAutomation(target);
        HashMap<String, List<Integer>> result = aca.find(text);
         
        System.out.println(text);
        for(Entry<String, List<Integer>> entry : result.entrySet()){
            System.out.println(entry.getKey()+" : " + entry.getValue());
        }
         
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,529评论 5 475
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,015评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,409评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,385评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,387评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,466评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,880评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,528评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,727评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,528评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,602评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,302评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,873评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,890评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,132评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,777评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,310评论 2 342

推荐阅读更多精彩内容

  • 参考https://www.cnblogs.com/cmmdc/p/7337611.html 首先简要介绍一下AC...
    idella阅读 550评论 0 0
  • AC自动机 AC自动机:Aho-Corasick automation,该算法在1975年产生于贝尔实验室,是著名...
    尼桑麻阅读 1,440评论 0 0
  • Aho-Corasick automation,该算法在1975年产生于贝尔实验室,是著名的多模匹配算法之一。一个...
    雨落八千里阅读 325评论 0 1
  • 参考博文:AC自动机算法详解 (转载) (原文作者:DarkRaven,原文的链接失效了)图片来源:AC自动机算...
    jenye_阅读 4,626评论 2 4
  • 参考资料:AC自动机GIF动图(来自油管) 以下文章节选自:王争老师 AC自动机:如何用多模式串匹配实现敏感词过滤...
    RainingMan阅读 2,978评论 0 0