AC自动机

简介: AC自动机

AC自动机解决的问题

在前缀树上玩KMP,

假设一个字符串数组中装有若干敏感词,给定一篇大文章,AC自动机就是查出大文章中包含了哪些敏感词

AC自动机是很多类似匹配算法的一个最基本的东西

步骤

先把若干敏感词建成前缀树,(前缀树,,

敏感词之间的组织就是一颗前缀树,但是要在前缀树上面做升级

在这里插入图片描述

需要在每个结点上加一条fail指针(fail指针用虚线表示),头结点的fail指针指向空,并且人为规定头结点的下级直接结点的fail指针全都指向头结点,

在这里插入图片描述
然后底层的fail指针如何设置?

先建完前缀树,在设置fail指针,根据宽度优先的顺序设置每一个结点的fail指针,

下图中,X结点的父结点有一条走向自己的路b,但是X的父结点的fail指针指的是头结点,头结点有走向a结点、b结点、c结点的路,就是说头结点没有走向b方向的路;于是头结点顺着fail指针往下走,来到null,null结点当然也没有走向b方向的路,所以x的fail指针直接指向头结点

在这里插入图片描述

然后设置下图中X结点的fail指针,跟上面一样,父结点到自己方向的路是d,但是从父结点顺着fail指针找不到走向d方向的路,所以X结点的fail指针直接指向头结点

在这里插入图片描述

接着设置下图中X结点的fail指针(按宽度优先的顺序设置),X的父走向自己的路位c,父的fail指针指向头结点,此时头结点有走向c方向的路所以X的fail指针就指向此时走到c方向的结点

在这里插入图片描述

fail指针设置规则

1)头结点的 fail 指针指向null;

2)头结点的下一级子节点的 fail 指针指向头结点;

3) 第3)种情况比较复杂;

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

AC自动机fail指针的实质含义

如果匹配失败,在剩下的字符串中,哪一个字符串的前缀 跟 必须包含当前字符的后缀匹配得最长,

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

代码

前缀树中的结点

在这里插入图片描述

在这里插入图片描述

完整代码
package com.harrison.class22;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;

/**
 * @author Harrison
 * @create 2022-04-02-10:21
 * @motto 众里寻他千百度,蓦然回首,那人却在灯火阑珊处。
 */
public class Code03_ACAutomaton {
    // 前缀树的结点
    public static class Node{
        // 如果一个Node,end为空,说明不是结尾
        // 如果end不为空,表示这个点是某个字符串的结尾,end的值就是这个字符串
        public String end;
        // 只有在上面的end变量不为空的时候,endUse才有意义
        // 表示,这个字符串之前没有加过答案,防止重复收集答案
        public boolean endUse;
        public Node fail;
        // 前缀树下级的路,假设大文章,所有敏感词都是小写,就可以固定26长度
        public Node[] nexts;

        public Node(){
            endUse=false;
            end=null;
            fail=null;
            nexts=new Node[26];
        }
    }

    public static class ACAutomaton{
        private Node root;

        // 对于AC自动机来说字符一定是放在路上的,而不是放在结点上,跟前缀树一样
        public ACAutomaton(){
            root=new Node();
        }

        // 建前缀树的过程,有多少个敏感词就调用多少次insert方法
        // 前缀树建好后,再调用build方法设置好所有的fail指针
        public void insert(String s){
            char[] str=s.toCharArray();
            Node cur=root;
            int index=0;
            for(int i=0; i<str.length; i++){
                index=str[i]-'a';
                if(cur.nexts[index]==null){
                    cur.nexts[index]=new Node();
                }
                cur=cur.nexts[index];
            }
            cur.end=s;
        }

        // 注意:不是设置弹出结点自己的fail指针
        // 而是弹出结点给它所有的子去设置fail指针
        public void build(){
            Queue<Node> queue=new LinkedList<>();
            queue.add(root);
            Node cur=null;
            Node cfail=null;
            while(!queue.isEmpty()){
                // 当前结点弹出,当前结点所有的后代加入队列,当前结点给它的子去设置fail指针
                // 某个父亲 cur
                cur=queue.poll();
                for(int i=0; i<26; i++){// 所有的路
                    // cur->父亲  i号儿子,必须把i号儿子的fail指针设置好
                    if(cur.nexts[i]!=null){// 如果真的有i号儿子
                        cur.nexts[i].fail=root;// 先设置成root,如果找到了再设置成别人
                        cfail=cur.fail;
                        while (cfail!=null){
                            if(cfail.nexts[i]!=null){
                                cur.nexts[i].fail=cfail.nexts[i];
                                break;
                            }
                            cfail=cfail.fail;
                        }
                        queue.add(cur.nexts[i]);
                    }
                }
            }
        }

        // 大文章
        public List<String> containWords(String content){
            char[] str=content.toCharArray();
            Node cur=root;
            Node follow=null;
            int index=0;
            List<String> ans=new ArrayList<>();
            for(int i=0; i<str.length; i++){
                index=str[i]-'a';// 路
                // 如果当前字符在这条路上没配出来,就随着fail方向走向下条路径
                while(cur.nexts[index]==null && cur!=root){
                    cur=cur.fail;
                }
                // 1) 现在来到的路径,是可以继续匹配的
                // 2) 现在来到的节点,就是前缀树的根节点
                cur=cur.nexts[index]!=null?cur.nexts[index]:root;
                follow=cur;
                while(follow!=root){
                    if(follow.endUse){
                        break;
                    }
                    // 不同的需求,在这一段之间修改
                    if(follow.end!=null){
                        ans.add(follow.end);
                        follow.endUse=true;
                    }
                    // 不同的需求,在这一段之间修改
                    follow=follow.fail;
                }
            }
            return ans;
        }
    }

    public static void main(String[] args) {
        ACAutomaton ac = new ACAutomaton();
        ac.insert("dhe");
        ac.insert("he");
        ac.insert("abcdheks");
        // 设置fail指针
        ac.build();

        List<String> contains = ac.containWords("abcdhekskdjfafhasldkflskdjhwqaeruv");
        for (String word : contains) {
            System.out.println(word);
        }
    }
}
相关文章
|
7月前
|
C++
AC自动机
AC自动机
34 0
|
20天前
|
人工智能 算法 Java
每日一刷《剑指offer》字符串篇之编辑距离
每日一刷《剑指offer》字符串篇之编辑距离
50 0
每日一刷《剑指offer》字符串篇之编辑距离
|
10月前
|
算法
一招教你看懂KMP算法next数组
一招教你看懂KMP算法next数组
89 0
|
10月前
|
存储 算法 调度
【喜闻乐见,包教包会】二分图最大匹配:匈牙利算法(洛谷P3386)
【喜闻乐见,包教包会】二分图最大匹配:匈牙利算法(洛谷P3386)
53 0
|
10月前
|
算法 测试技术 Android开发
LeetCode 周赛上分之旅 # 36 KMP 字符串匹配殊途同归
学习数据结构与算法的关键在于掌握问题背后的算法思维框架,你的思考越抽象,它能覆盖的问题域就越广,理解难度也更复杂。在这个专栏里,小彭与你分享每场 LeetCode 周赛的解题报告,一起体会上分之旅。
59 0
|
11月前
|
算法 搜索推荐 编译器
字符串算法
字符串算法是指用于处理字符串的算法。在计算机科学和软件开发中,字符串算法是非常重要的,它们被广泛应用于文本编辑、搜索引擎、编译器、数据库系统等各个领域。以下是一些常见的字符串算法及其实现方法和示例代码:
77 1
|
存储 算法 BI
KMP算法(kmp) next数组算法解析
KMP算法(kmp) next数组算法解析
120 0
KMP算法(kmp) next数组算法解析
|
算法 C++
[解题报告](第22讲) 字符串算法(二) - 字符串比较
[解题报告](第22讲) 字符串算法(二) - 字符串比较
[解题报告](第22讲) 字符串算法(二) - 字符串比较
|
算法 网络协议
[解题报告](第23讲) 字符串算法(三) - 字符串分割
[解题报告](第23讲) 字符串算法(三) - 字符串分割
[解题报告](第23讲) 字符串算法(三) - 字符串分割