多模式匹配AC自动机及其应用

BBS等文本内容网站,大都会有敏感词过滤功能,用来过滤掉用户输入的一些淫秽、反动、谩骂等内容。

实际上,这些功能最基本的原理就是字符串匹配算法,也就是通过维护一个敏感词的字典,当用户输入一段文字内容之后,通过字符串匹配算法,来查找用户输入的这段文字,是否包含敏感词。如果有,就用“***”把它替代掉。

 

单模式字符串匹配算法都可以处理这个问题。但是,对于访问量巨大的网站来说,比如淘宝,用户每天的评论数有几亿、甚至几十亿。这时候,对敏感词过滤系统的性能要求就要很高。

 

1、基于单模式串和 Trie 树实现的敏感词过滤

常见的字符串匹配算法,有 BF 算法、RK 算法、BM 算法、KMP 算法,还有 Trie 树。前面四种算法都是单模式串匹配算法,只有 Trie 树是多模式串匹配算法。

 

单模式串匹配算法,是在一个模式串和一个主串之间进行匹配,也就是说,在一个主串中查找一个模式串。

多模式串匹配算法,就是在多个模式串和一个主串之间做匹配,也就是说,在一个主串中查找多个模式串。

 

 

在敏感词需求中,我们可以对敏感词字典进行预处理,构建成 Trie 树结构。预处理只需要做一次,如果敏感词字典动态更新了,比如删除、添加了一个敏感词,那我们只需要动态更新一下 Trie 树就可以了。

 

 

2、经典的多模式串匹配算法:AC 自动机

 

AC自动机比Trie树牛在哪里,其实就是添加了fail指针可以减少匹配次数。

 

AC 自动机算法,全称是 Aho-Corasick 算法。其实,Trie 树跟 AC 自动机之间的关系,就像单串匹配中朴素的串匹配算法,跟 KMP 算法之间的关系一样,只不过前者针对的是多模式串而已。所以,AC 自动机实际上就是在 Trie 树之上,加了类似 KMP 的 next 数组,只不过此处的 next 数组是构建在树上罢了。

 

如果代码表示,就是下面这个样子:

 

public class AcNode {
  public char data; 
  public AcNode[] children = new AcNode[26]; // 字符集只包含 a~z 这 26 个字符
  public boolean isEndingChar = false; // 结尾字符为 true
  public int length = -1; // 当 isEndingChar=true 时,记录模式串长度
  public AcNode fail; // 失败指针
  public AcNode(char data) {
    this.data = data;
  }
}

  

 

所以,AC 自动机的构建,包含两个操作:

  • 将多个模式串构建成 Trie 树;
  • 在 Trie 树上构建失败指针(相当于 KMP 中的失效函数 next 数组)。

 

3、经典AC自动机代码实现

 

LeetCode1032是一道典型的多模式匹配题目。

class StreamChecker {
    TrieNode root;
    TrieNode temp;
    public StreamChecker(String[] words) {
        root = new TrieNode();
        for (String word : words) {
            TrieNode cur = root;
            for (int i = 0; i < word.length(); i++) {
                int index = word.charAt(i) - 'a';
                if (cur.getChild(index) == null) {
                    cur.setChild(index, new TrieNode());
                }
                cur = cur.getChild(index);
            }
            cur.setIsEnd(true);
        }
        root.setFail(root);
        Queue<TrieNode> q = new LinkedList<>();
        for (int i = 0; i < 26; i++) {
            if (root.getChild(i) != null) {
                root.getChild(i).setFail(root);
                q.add(root.getChild(i));
            } else {
                root.setChild(i, root);
            }
        }
        while (!q.isEmpty()) {
            TrieNode node = q.poll();
            node.setIsEnd(node.getIsEnd() || node.getFail().getIsEnd());
            for (int i = 0; i < 26; i++) {
                if (node.getChild(i) != null) {
                    node.getChild(i).setFail(node.getFail().getChild(i));
                    q.offer(node.getChild(i));
                } else {
                    node.setChild(i, node.getFail().getChild(i));
                }
            }
        }
        temp = root;
    }
    public boolean query(char letter) {
        temp = temp.getChild(letter - 'a');
        return temp.getIsEnd();
    }
}
class TrieNode {
    TrieNode[] children;
    boolean isEnd;
    //添加失败指针
    TrieNode fail;
    public TrieNode() {
        children = new TrieNode[26];
    }
    public TrieNode getChild(int index) {
        return children[index];
    }
    public void setChild(int index, TrieNode node) {
        children[index] = node;
    }
    public boolean getIsEnd() {
        return isEnd;
    }
    public void setIsEnd(boolean b) {
        isEnd = b;
    }
    public TrieNode getFail() {
        return fail;
    }
    public void setFail(TrieNode node) {
        fail = node;
    }
}

 

posted @ 2023-04-25 17:28  邴越  阅读(32)  评论(0编辑  收藏  举报