AC自动机

前置知识

使用场景

AC自动机是一种著名的多模式匹配算法。

可以完成类似于KMP算法的工作,但是由单字符串的匹配变成了多字符串的匹配。

一般来说,会有很多子串,和一个母串。问题常是求字串在母串中的出现情况(包括位置,次数,等等)

算法思想与流程

我在Trie树一文中提到过这样一句话

而AC自动机的核心就在于通过对Trie树进行处理,使得在处理母串的信息时可以快速的进行状态转移。

可以类比KMP的算法流程,但是这不重要

例如子串有 aa, ab, abc, b。母串为 ababcba

由于我们是通过母串进行状态转移,所以需要先把所有字串的信息搞定

我们可以先处理子串,建一棵Trie树

明显,对于一个字串的匹配,是不可能在树上一路到底的,所以要构建匹配失败时的回退机制。也就是需要构建失配指针。

那么失配指针是干什么的?也就是用来在 Trie 树上向上跳,找到可以转移的一个节点,进行状态转移。

假如我现在在3号节点,并且我下一个需要转移的状态是 b,很明显,我此时应该回退到1节点(其上第一个可以通过 b 转移的节点)并转移到4节点。如果再来一个 b,也只能向上走到0号节点,然后转移到2号节点。

如此看来,我们完全可以暴力向上跳找到可转移的状态或者到达根为止。但是,这明显不够优秀,我们完全可以继承其子节点的。也就是继承 fail 的子节点。使得不需要暴力向上跳。

那说了半天,fail 到底指向啥?

假设父节点到当前节点转移的状态为 x,父节点之上第一个可以通过 x 转移到下一个节点的节点为 u,则 fail 指向 u 通过 x 转移过后的节点。

其实还有另一种解释的方法

fail 指向 p 代表当前串的最长已知后缀。

例如 aa 的最长已知后缀为 a,所以 3号节点的 fail 指向 1号节点;abc 的最长已知后缀为空,所以 5 号节点的 fail 指向根节点。

好混乱,我尽力了……

那么核心代码……就是利用 BFS 来处理

  1. void procFail(int * q) {
  2. int head(0), tail(0);
  3. for (int i(0); i < 26; ++i) {
  4. if (kids[0][i]) q[tail++] = kids[0][i];
  5. }
  6. while (head ^ tail) {
  7. int x = q[head++];
  8. for (int i(0); i < 26; ++i) {
  9. if (kids[x][i]) {
  10. fail[kids[x][i]] = kids[fail[x]][i];
  11. q[tail++] = kids[x][i];
  12. } else kids[x][i] = kids[fail[x]][i];
  13. }
  14. } // procFail end
  15. }

注意事项:一般来说,把 0 号作为根节点会比较方便。反正 0 上不可能有信息保存。

插入部分我就不需要讲了

匹配的判断

如何判断当前状态有没有匹配任何一个字串,只需要不断向上跳 fail,看跳到的节点是不是代表着字串。

拿模板:【模板】AC 自动机(简单版) - 洛谷 为例。

插入的时候在最后标记一下有没有匹配:

  1. void insert(string &s) {
  2. int p(0);
  3. for (int c : s) {
  4. if (!kids[p][(c -= 'a')]) kids[p][c] = ++usage;
  5. p = kids[p][c];
  6. }
  7. ++cnt[p];
  8. }

在匹配的时候暴力跳就是了:

  1. int ACMatch(string & s) {
  2. int p(0), ans(0);
  3. for (int c : s) {
  4. p = kids[p][(c -= 'a')];
  5. for (int t(p); t && ~cnt[t]; t = fail[t]) {
  6. ans += cnt[t], cnt[t] = -1;
  7. }
  8. }
  9. return ans;
  10. }

由于每一个串只能匹配一次,所以这里采用的清空的策略。并且标记清空,以免重复搜索。

失配树的应用

就拿模板题来说吧:【模板】AC 自动机(二次加强版) - 洛谷

他是要求所有字串的出现情况。

那么,我们先把每一个到达的状态计数。再通过 fail 指针向上跳求和。

但毕竟不能每一个节点都暴力跳,所以考虑在 fail 树上求和。

但是,我们不是有一个 qBFS 吗?其中的 fail 是有序的:对于一个节点 x,其 fail 一定在 x 之前被遍历到。

所以我们直接使用 q 即可。

那么合起来大概也就是这样:

  1. inline void ACMatch(string &s) {
  2. int p(0);
  3. for (char c : s) {
  4. p = kids[p][c - 'a'];
  5. ++cnt[p];
  6. }
  7. }
  8. inline void ACCount(int * q) {
  9. for (int i = usage; i; --i) {
  10. cnt[fail[q[i]]] += cnt[q[i]];
  11. }
  12. }

但是每一个特定的字串出现的次数呢?

在插入时记住字串对应的节点,输出即可。

  1. void insert(string &s, int i) {
  2. int p(0);
  3. for (int c : s) {
  4. if (!kids[p][(c -= 'a')]) kids[p][c] = newNode();
  5. p = kids[p][c];
  6. }
  7. pos[i] = p;
  8. }
  9. inline void ACOutput(int n) {
  10. for (int i = 1; i <= n; ++i) {
  11. cout << cnt[pos[i]] << '\n';
  12. }
  13. }

有这么一道题:

很明显,对于每一个位置,我们需要清理能匹配到的最长长度,所以我们需要预处理出最长长度:

  1. inline void ACprepare(int * q) {
  2. for (int i = 1; i <= usage; ++i) {
  3. len[q[i]] = max(len[q[i]], len[fail[q[i]]]);
  4. }
  5. }

在清理时:

  1. inline void ACclean(string &s) {
  2. int p(0);
  3. for (unsigned i(0), ie = s.size(); i < ie; ++i) {
  4. p = kids[p][discrete(s[i])];
  5. if (len[p]) for (unsigned j = i - len[p] + 1; j <= i; ++j)
  6. s[j] = '*';
  7. }
  8. }

由于是引用的字符串,所以可以直接修改。

对状态的理解

在我们考试的时候有这么一道题:

这道题说难也难,说不难也不难。主要是看对于 AC自动机 状态转移的理解到不到位。

在匹配过程中,如果匹配到了出现的 w,那么就要回到 len(w) 个状态前,继续匹配下一个字符。

很明显,需要用栈,并且由于需要一次弹出多个,所以最好用手写的栈。

核心代码如下:

  1. string sub, pat;
  2. cin >> sub >> pat;
  3. insert(sub), procFail(Q);
  4. int p = 0;
  5. for (int i(0), ie = pat.size(); i < ie; ++i) {
  6. p = kids[cps[ci]][pat[i] - 'a'];
  7. cps[++ci] = p, ccs[ci] = pat[i];
  8. if (match[p]) ci -= sub.size();
  9. }
  10. for (int i = 1; i <= ci; ++i) {
  11. putchar(ccs[i]);
  12. }

这里没有用到 fail,那么为什么还要构建失配树?

这是个好问题,因为,构建失配树的过程不仅仅构建了失配树,同时还令节点继承了其 fail 的子节点,所以需要构建的过程。


最后附上模板题【模板】AC 自动机(二次加强版) - 洛谷的代码:

  1. #include <iostream>
  2. #include <algorithm>
  3. #include <string>
  4. using namespace std;
  5. const int N = 1e6 + 7;
  6. int res[N], cnt[N], pos[N];
  7. class ACAutomaton {
  8. private:
  9. int kids[N][26];
  10. int fail[N], id[N], usage;
  11. public:
  12. ACAutomaton() : usage(0) {
  13. }
  14. inline int newNode() {
  15. fill_n(kids[++usage], 26, 0);
  16. cnt[usage] = fail[usage] = id[usage] = 0;
  17. return usage;
  18. }
  19. void insert(string &s, int i) {
  20. int p(0);
  21. for (int c : s) {
  22. if (!kids[p][(c -= 'a')]) kids[p][c] = newNode();
  23. p = kids[p][c];
  24. }
  25. pos[i] = p;
  26. }
  27. void procFail(int * q) {
  28. int head(0), tail(0);
  29. for (int i(0); i < 26; ++i) {
  30. if (kids[0][i])
  31. fail[kids[0][i]] = 0, q[tail++] = kids[0][i];
  32. }
  33. while (head ^ tail) {
  34. int x = q[head++];
  35. for (int i(0); i < 26; ++i) {
  36. if (kids[x][i]) {
  37. fail[kids[x][i]] = kids[fail[x]][i];
  38. q[tail++] = kids[x][i];
  39. } else kids[x][i] = kids[fail[x]][i];
  40. }
  41. } // procFail end
  42. }
  43. void debug() {
  44. for (int i = 0; i <= usage; ++i) {
  45. printf("node %d (cnt %d) fail to %d:\n\t", i, cnt[i], fail[i]);
  46. for (int j(0); j < 26; ++j) {
  47. printf("%d ", kids[i][j]);
  48. } puts("");
  49. }
  50. }
  51. inline void ACMatch(string &s) {
  52. int p(0);
  53. for (char c : s) {
  54. p = kids[p][c - 'a'];
  55. ++cnt[p];
  56. }
  57. }
  58. inline void ACCount(int * q) {
  59. for (int i = usage; i; --i) {
  60. cnt[fail[q[i]]] += cnt[q[i]];
  61. }
  62. }
  63. inline void ACOutput(int n) {
  64. for (int i = 1; i <= n; ++i) {
  65. cout << cnt[pos[i]] << '\n';
  66. }
  67. }
  68. void clear() {
  69. usage = -1;
  70. newNode(); // clear 0
  71. }
  72. } ac;
  73. int Q[N];
  74. string s;
  75. int main() {
  76. cin.tie(0)->sync_with_stdio(false);
  77. int n;
  78. cin >> n;
  79. for (int i = 1; i <= n; ++i) {
  80. cin >> s;
  81. ac.insert(s, i);
  82. } ac.procFail(Q);
  83. cin >> s;
  84. ac.ACMatch(s);
  85. ac.ACCount(Q);
  86. ac.ACOutput(n);
  87. return 0;
  88. }

差不多了……下课

算法学习笔记(20): AC自动机的更多相关文章

  1. 学习笔记:AC自动机

    话说AC自动机有什么用......我想要自动AC机 AC自动机简介:  首先简要介绍一下AC自动机:Aho-Corasick automation,该算法在1975年产生于贝尔实验室,是著名的多模匹配 ...

  2. 【学习笔记】ac自动机&fail树

    定义 解决文本串和多个模式串匹配的问题: 本质是由多个模式串形成的一个字典树,由tie的意义知道:trie上的每一个节点都是一个模式串的前缀: 在trie上加入fail边,一个节点fail边指向这个节 ...

  3. SQL反模式学习笔记20 明文密码

    目标:恢复或重置密码 反模式:使用明文存储密码 1.存储密码 使用明文存储密码或者在网络上传递密码是不安全的. 如果攻击者截取到你用来插入(或者修改)密码的sql语句,就可以获得密码.     黑客获 ...

  4. 某科学的PID算法学习笔记

    最近,在某社团的要求下,自学了PID算法.学完后,深切地感受到PID算法之强大.PID算法应用广泛,比如加热器.平衡车.无人机等等,是自动控制理论中比较容易理解但十分重要的算法. 下面是博主学习过程中 ...

  5. Miller-Rabin 与 Pollard-Rho 算法学习笔记

    前言 Miller-Rabin 算法用于判断一个数 \(p\) 是否是质数,若选定 \(w\) 个数进行判断,那么正确率约是 \(1-\frac{1}{4^w}\) ,时间复杂度为 \(O(\log ...

  6. Ext.Net学习笔记20:Ext.Net FormPanel 复杂用法

    Ext.Net学习笔记20:Ext.Net FormPanel 复杂用法 在上一篇笔记中我们介绍了Ext.Net的简单用法,并创建了一个简单的登录表单.今天我们将看一下如何更好是使用FormPanel ...

  7. C / C++算法学习笔记(8)-SHELL排序

    原始地址:C / C++算法学习笔记(8)-SHELL排序 基本思想 先取一个小于n的整数d1作为第一个增量(gap),把文件的全部记录分成d1个组.所有距离为dl的倍数的记录放在同一个组中.先在各组 ...

  8. Manacher算法学习笔记 | LeetCode#5

    Manacher算法学习笔记 DECLARATION 引用来源:https://www.cnblogs.com/grandyang/p/4475985.html CONTENT 用途:寻找一个字符串的 ...

  9. golang学习笔记20 一道考察对并发多协程操作一个共享变量的面试题

    golang学习笔记20 一道考察对并发多协程操作一个共享变量的面试题 下面这个程序运行的能num结果是什么? package main import ( "fmt" " ...

  10. Johnson算法学习笔记

    \(Johnson\)算法学习笔记. 在最短路的学习中,我们曾学习了三种最短路的算法,\(Bellman-Ford\)算法及其队列优化\(SPFA\)算法,\(Dijkstra\)算法.这些算法可以快 ...

随机推荐

  1. 使用 GIT Bash Here 打tar包文件

    1.进入要被  打包的文件目录下 2.点击  Git Bash Here  ---> tar cvf server.tar server/ ok!!!!!!

  2. 树莓派4B的Node-Red编程(一)

    一.树莓派烧写 二.Node-Red 环境搭建 (一)安装Node.js (二)安装Node-Red (三)启动服务:win+R输入CMD:输入Node-red. (四)进入浏览器127.0.0.1: ...

  3. 进入容器后不显示id

    https://www.656463.com/wenda/dockerexejrrqbxsrqID_493 net=host的原因

  4. Jetpack compose学习笔记之自定义layout(布局)

    一,简介 Compose中的自定义Layout主要通过LayoutModifier和Layout方法来实现. 不管是LayoutModifier还是Layout,都只能measure一次它的孩子Vie ...

  5. Debug --> 使用USTC-TK2016工具对USTC-TFC2016数据集进行处理

    文件介绍: https://blog.csdn.net/u010916338/article/details/86511009?spm=1001.2101.3001.6661.1&utm_me ...

  6. vue搭建项目iview+axios+less

    项目地址:https://github.com/CinderellaStory/vue-iview-project vue搭建项目壳子已安装:iview.axios.less 已有界面:登录.左侧菜单 ...

  7. ansible 详解基本篇

    Ansible是一种常用的自动运维化工具,基于python开发,分布式,无需客户端,轻量级,配置语言采用YAML. 安装方式yum yum install epel-release&& ...

  8. vue3.0+vite按需引入element plus

    1.安装vite-plugin-style-import yarn add vite-plugin-style-import -D 2.在项目根目录下的vite.config.js中配置 import ...

  9. 修改AXI UART D16550 FIFO深度的过程记录

    仅限于AXI UART 16550 v. 2.0,其他版本可能存在差异,经过实际测试,可以将fifo深度从默认的16成功修改为32.128和256.参考了两篇帖子中提到的方法,分别是修改AXI UAR ...

  10. 链表反转,C++实现

    1 // To Compile and Run: g++ invert_list.cc -std=c++11 -Wall -O3 && ./a.out 2 3 4 #include & ...