AC自动机

参考资料:AC自动机GIF动图(来自油管)

以下文章节选自:王争老师 AC自动机:如何用多模式串匹配实现敏感词过滤功能?

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 数组)。

关于如何构建 Trie 树,请参考Trie树的部分。所以,这里我们就重点看下,构建好 Trie 树之后,如何在它之上构建失败指针?
我用一个例子给你讲解。这里有 4 个模式串,分别是 c,bc,bcd,abcd;主串是 abcd。


Trie 树中的每一个节点都有一个失败指针,它的作用和构建过程,跟 KMP 算法中的 next 数组极其相似。所以要想看懂这节内容,你要先理解 KMP 算法中 next 数组的构建过程。如果你还有点不清楚,建议你先回头去弄懂 KMP 算法。

假设我们沿 Trie 树走到 p 节点,也就是下图中的紫色节点,那 p 的失败指针就是从 root 走到紫色节点形成的字符串 abc,跟所有模式串前缀匹配的最长可匹配后缀子串,就是箭头指的 bc 模式串。

这里的最长可匹配后缀子串,我稍微解释一下。字符串 abc 的后缀子串有两个 bc,c,我们拿它们与其他模式串匹配,如果某个后缀子串可以匹配某个模式串的前缀,那我们就把这个后缀子串叫作可匹配后子串

我们从可匹配后缀子串中,找出最长的一个,就是刚刚讲到的最长可匹配后缀子串。我们将 p 节点的失败指针指向那个最长匹配后缀子串对应的模式串的前缀的最后一个节点,就是下图中箭头指向的节点。



计算每个节点的失败指针这个过程看起来有些复杂。其实,如果我们把树中相同深度的节点放到同一层,那么某个节点的失败指针只有可能出现在它所在层的上一层。

我们可以像 KMP 算法那样,当我们要求某个节点的失败指针的时候,我们通过已经求得的、深度更小的那些节点的失败指针来推导。也就是说,我们可以逐层依次来求解每个节点的失败指针。所以,失败指针的构建过程,是一个按层遍历树的过程。

首先 root 的失败指针为 NULL,也就是指向自己。当我们已经求得某个节点 p 的失败指针之后,如何寻找它的子节点的失败指针呢?

我们假设节点 p 的失败指针指向节点 q,我们看节点 p 的子节点 pc 对应的字符,是否也可以在节点 q 的子节点中找到。如果找到了节点 q 的一个子节点 qc,对应的字符跟节点 pc 对应的字符相同,则将节点 pc 的失败指针指向节点 qc。



如果节点 q 中没有子节点的字符等于节点 pc 包含的字符,则令 q=q->fail(fail 表示失败指针,这里有没有很像 KMP 算法里求 next 的过程?),继续上面的查找,直到 q 是 root 为止,如果还没有找到相同字符的子节点,就让节点 pc 的失败指针指向 root。



我将构建失败指针的代码贴在这里,你可以对照着讲解一块看下,应该更容易理解。这里面,构建 Trie 树的代码我并没有贴出来,你可以参看上一节的代码,自己实现。
public void buildFailurePointer() {
  Queue<AcNode> queue = new LinkedList<>();
  root.fail = null;
  queue.add(root);
  while (!queue.isEmpty()) {
    AcNode p = queue.remove();
    for (int i = 0; i < 26; ++i) {
      AcNode pc = p.children[i];
      if (pc == null) continue;
      if (p == root) {
        pc.fail = root;
      } else {
        AcNode q = p.fail;
        while (q != null) {
          AcNode qc = q.children[pc.data - 'a'];
          if (qc != null) {
            pc.fail = qc;
            break;
          }
          q = q.fail;
        }
        if (q == null) {
          pc.fail = root;
        }
      }
      queue.add(pc);
    }
  }
}

通过按层来计算每个节点的子节点的失效指针,刚刚举的那个例子,最后构建完成之后的 AC 自动机就是下面这个样子:


AC 自动机到此就构建完成了。我们现在来看下,如何在 AC 自动机上匹配主串?

我们还是拿之前的例子来讲解。在匹配过程中,主串从 i=0 开始,AC 自动机从指针 p=root 开始,假设模式串是 b,主串是 a。

  • 如果 p 指向的节点有一个等于 b[i] 的子节点 x,我们就更新 p 指向 x,这个时候我们需要通过失败指针,检测一系列失败指针为结尾的路径是否是模式串。这一句不好理解,你可以结合代码看。处理完之后,我们将 i 加一,继续这两个过程;
  • 如果 p 指向的节点没有等于 b[i] 的子节点,那失败指针就派上用场了,我们让 p=p->fail,然后继续这 2 个过程。

关于匹配的这部分,文字描述不如代码看得清楚,所以我把代码贴了出来,非常简短,并且添加了详细的注释,你可以对照着看下。这段代码输出的就是,在主串中每个可以匹配的模式串出现的位置。

public void match(char[] text) { // text 是主串
  int n = text.length;
  AcNode p = root;
  for (int i = 0; i < n; ++i) {
    int idx = text[i] - 'a';
    while (p.children[idx] == null && p != root) {
      p = p.fail; // 失败指针发挥作用的地方
    }
    p = p.children[idx];
    if (p == null) p = root; // 如果没有匹配的,从 root 开始重新匹配
    AcNode tmp = p;
    while (tmp != root) { // 打印出可以匹配的模式串
      if (tmp.isEndingChar == true) {
        int pos = i-tmp.length+1;
        System.out.println(" 匹配起始下标 " + pos + "; 长度 " + tmp.length);
      }
      tmp = tmp.fail;
    }
  }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • 参考博文:AC自动机算法详解 (转载) (原文作者:DarkRaven,原文的链接失效了)图片来源:AC自动机算...
    jenye_阅读 4,631评论 2 4
  • 参考https://www.cnblogs.com/cmmdc/p/7337611.html 首先简要介绍一下AC...
    idella阅读 550评论 0 0
  • AC自动机(Aho-Corasick\ automaton),可以解决多模板串匹配的问题。可以理解为可以一次性匹配...
    An_Account阅读 471评论 0 1
  • 文不达意,口齿不清,思想混乱,令人喷饭。(估计只有我自己才能看懂我在说什么)简书没有mathjax公式没法愉快显示...
    njzwj阅读 1,170评论 1 2
  • A 日历 本周我是否完成重要的日历事项,有没有爽约,或变更固定事项 青蛙有的未完成 B 清单 本周我有没有做到要事...
    转角遇到爱人阅读 112评论 0 0