发现已经忘了许多。。。。于是复习一下

基础要点概况

  • AC 自动机基于 Trie 树 的结构,即构建 AC 自动机前需要先建 Trie。

  • 一个状态中除了转移 \(\delta\) 之外还有失配指针 \(fail\)。\(fail(x)\) 对于的字符串是 \(x\) 对应字符串的 最长真后缀

  • 要求出 \(fail\) 我们可以 bfs 实现。对于当前状态 \(x\),设其父亲 \(f\) 通过一个 \(c\) 转移连向 \(x\),那么我们先看看 \(fail(f)\) 是否存在 \(c\) 转移,如果有那么 \(fail(x)\gets \delta(fail(f),c)\),否则就看 \(fail(fail(f))\),再不行继续递归下去。要真没有就直接指向根状态。

  • 但实际上我们都会直接写成 Trie 图,如果一个转移 \(\delta(x, c)\) 不存在,那么就 \(\delta(x, c)\gets\delta(fail(x), c)\)。从而一些查询 & 构建 的时候就根本不用直接跳 \(fail\) 应付失配,优化了效率和代码难度。

  • 构建 AC 自动机的代码非常简洁(复杂度 \(O(n\times |\Sigma|)\),\(n\) 为状态个数,下同):

    void init_fail() {
    for (int i = 0; i < S; i++) ch[0][i] = 1;
    for (Q.push(1); !Q.empty(); ) {
    int x = Q.front(); Q.pop();
    for (int i = 0; i < S; i++)
    if (!ch[x][i]) ch[x][i] = ch[fail[x]][i];
    else Q.push(ch[x][i]), fail[ch[x][i]] = ch[fail[x]][i];
    }
    }
  • AC 自动机最经典的应用就是 多模式串匹配 了:Luogu P3808 【模板】AC自动机(简单版)。先对所有模式串建 AC 自动机,然后从根开始跑文本串:每到达一个点,沿着自己的 \(fail\) 向上跳一遍,答案加上沿途遇到的终止状态的个数。当然,为避免重复统计,可以给走过的位置打一个标记。

  • 询问的参考代码(复杂度 \(O(n)\)):

    int query(char* s) {
    int ans = 0;
    for (int x = 1, i = 0; s[i]; i++) {
    x = ch[x][s[i] - 'a'];
    for (int y = x; y && ~cnt[y]; y = fail[y])
    ans += cnt[y], cnt[y] = -1;
    }
    return ans;
    }
  • 然而这样做一些多次询问的题会被卡爆成 \(O(n\times Q)\),比如要求所有串分别出现的次数时,遇到 aaaaaa...aa 这种,一次转移就要跳 \(O(n)\) 次失配指针。于是引入 \(fail\) 树:对于每个非根状态 \(x\),都从 \(fail(x)\) 连过来一条边,最终形成 \(fail\) 树。

  • \(fail\) 树将我跳失配指针的过程实体化了,那么一个状态能更新到 \(x\),那么说明这个状态在 \(x\) 的 \(fail\) 树上的子树内。这就好办了,我们先将文本串在 AC 自动机上跑一边,沿途更新计数器,然后一个状态对应的 \(fail\) 树 子树和 即为出现次数。

  • 于是这样就是真的线性了,Luogu P5357 【模板】AC自动机(二次加强版) 代码:

    std::vector<int> adj[N];
    int dfs(int x) {
    for (int i = 0; i < (int)adj[x].size(); i++)
    cnt[x] += dfs(adj[x][i]);
    return cnt[x];
    }
    void query(char* s) {
    for (int x = 1, i = 0; s[i]; i++)
    ++cnt[x = ch[x][s[i] - 'a']];
    dfs(1);
    }

进阶应用(套路)

套路 1:AC 自动机相关 dp

【JSOI2007】文本生成器:给你若干个模式串,求至少包含一个模式串的长度为 \(m\) 的文本串个数。

  • 首先一个简单的容斥,答案为 \(m^{|\Sigma|}\) 减去不包含任何一个模式串的个数。

  • 然后令 \(f(i,j)\) 为当前长度为 \(i\) 且走到状态 \(j\) 的方案数。那么转移显然是 \(\forall \delta(j,c)\ne \text{null}: f(i,j) \to f(i+1,\delta(j,c))\),并且 不能转移到有结束标记 的状态。

  • 但这样还不行,要得到一个字符串,我们不只有这一个状态可以作为终点。如果当前代表的字符串的最长真后缀 \(fail(x)\) 不能走,那么当前状态 \(x\) 也不能走,因为 前者必然被后者所包含。那么考虑稍微更改一下构建的实现:

    void build() {
    std::queue<int> Q;
    for (int i = 0; i < S; i++) ch[0][i] = 1;
    for (Q.push(1); !Q.empty(); ) {
    int x = Q.front(); Q.pop();
    for (int i = 0; i < S; i++) {
    if (!ch[x][i]) { ch[x][i] = ch[fail[x]][i]; continue; }
    Q.push(ch[x][i]);
    fail[ch[x][i]] = ch[fail[x]][i];
    end[ch[x][i]] |= end[fail[ch[x][i]]]; // <-
    }
    }
    }
  • 那么 dp 的过程就比较显然了:先 \(1\to m\) 枚举长度,再考虑所有的状态,对于每个状态枚举所有可行转移。

    f[0][1] = 1;
    for (int i = 1; i <= m; i++)
    for (int j = 1; j <= total; j++)
    for (int c = 0; c < S; c++) if (!end[ch[j][c]])
    (f[i][ch[j][c]] += f[i - 1][j]) %= mod;
  • 时间复杂度 \(O(m\times \sum_i|s_i|)\)。

套路 2:套路 1 的矩阵优化

【POJ 2778】DNA Sequence:给定 \(n\) 个禁止串,求长度为 \(m\) 且不含任何一个禁止串的字符串个数。\(1\le m\le 2\times 10^9\)

  • 现在 \(m\) 的规模边的很大,怎么办?我们先把问题做一步转化:从根状态结点走 \(m\) 步到任意 非禁止状态 的方案数。那么我们将建出的 Trie 图看做一个 有向图。然后就是经典的“从 \(s\) 走 \(m\) 条边到 \(t\) 的走法数”问题。

  • 很显然地考虑 邻接矩阵(\(g\)) 表示这个图,然后对其做 \(m\) 次幂。那么 \(g_{i,j}\) 就是 \(i\) 走 \(m\) 步到达 \(j\) 的方案数。

  • 那么答案即为 \(\sum_{x\in{\text{ACAM}}}g_{Q,x}\),其中 \(Q\) 为根状态。

  • 再用 矩阵快速幂 优化幂运算,复杂度为 \(O((\sum_i|s_i|)^3\log m)\):

    Matrix f, g;
    for (int i = 1; i <= total; i++) if (!end[i])
    for (int j = 0; j < S; j++) if (!end[ch[i][j]])
    ++f.e[i][ch[i][j]];
    for (int i = 1; i <= total; i++)
    g.e[i][i] = 1; for (; m; m >>= 1, f = f * f)
    if (m & 1) g = g * f; int ans = 0;
    for (int i = 1; i <= total; i++)
    (ans += g.e[1][i]) %= mod;
    printf("%d\n", ans);

套路 3:转化为树上统计问题

【Codeforces 163E】e-Government:给定 \(k\) 个字符串 \(s_1, s_2, \cdots, s_k\),要求维护一个字符串集 \(S\),一开始 \(k\) 个字符串都在 \(S\) 中,现有 \(n\) 次操作,每次会加入或移除 \(k\) 个字符串中的一个,或者询问一个文本串求出 \(S\) 中每个串匹配次数之和。

  • 首先 AC 自动机并不能很方便地支持动态加,更何况删除,显然是一开始就要建好 AC 自动机。
  • 然后不能想到修改时直接在对应位置的计数器 \(\pm 1\),然后统计贡献直接暴跳 \(fail\)。然而这个 Naive 的想法早就被卡了。
  • 于是想二次加强版一样考虑建出 \(fail\) 树,然后就是跳祖先累加贡献,也就是 链上求和
  • 所以说现在要维护一颗树,支持链求和 & 单点修改。树剖或括号序加树状数组都可,复杂度 \(O(n\log n)/O(n\log^2 n)\)。
  • 以及 【NOI2011】阿狸的打字机 也用了类似的思想,推荐写一下。

杂题选做

【POI2000】病毒

给定 \(n\) 个禁止串,求是否存在无限长的串,不包含任意一个禁止串。

  • 这个题非常神奇,它要求尽量不匹配。
  • 于是我们将计就计,在 AC 自动机上跑的时候,尽量避开禁止状态。注意,这里“禁止”的处理也需要想“文本生成器”那样修改构建函数。
  • 然而“尽量避开”是个很模糊的概念,不过在这里显然是指可以在 AC 自动机下无限地走下去。
  • 那么,其实只要找到一个 经过根状态的环即可。一次 Dfs 搞定。

【Codeforces 1202E】You Are Given Some Strings...

给定一个字符串 \(t\) 以及 \(n\) 个模式串 \(s_1, s_2, \cdots, s_n\)。设 \(f(s, t)\) 为字符串 \(t\) 在 \(s\) 中的出现次数,\(s_i+s_j\) 表示 \(s_i\) 在后面追加 \(s_j\) 所得到的字符串。求 \(\sum_{i,j}f(t, s_i+s_j)\)。

  • 首先,如果其中一个 \(s_i+s_j\) 匹配上了,那么必然在 \(t\) 中存在一个 断点,使得前半部分的一个后缀为 \(s_i\),后半部分的一个前缀为 \(s_j\)。
  • 那么考虑枚举这个断点 \(x\),记 \(f(x)\) 为 \(t\) 的前缀 \(1\sim x\) 中有几个模式串可以作为其后缀,同理对后缀 \(x\sim |t|\) 定义 \(g\) 表示几个可以作为前缀。答案可以表示为 \(\sum_{i\in[1, n)} f(i)\times g(i+1)\)。
  • 由于 \(f\) 将字符串翻转就是 \(g\),这里只提一下 \(f\) 的求法。首先对 \(s_1,s_2, \cdots,s_n\) 建 AC 自动机,然后 \(t\) 在上面跑转移。走到一个位置,当前 \(f\) 的值就是 \(fail\) 树上的子树和。\(g\) 的话就把所有串翻转再跑一遍。
  • 复杂度 \(O(\sum_i|s_i|+|t|)\)

【复习笔记】重习 AC 自动机的更多相关文章

  1. 「笔记」AC 自动机

    目录 写在前面 定义 引入 构造 暴力 字典图优化 匹配 在线 离线 复杂度 完整代码 例题 P3796 [模板]AC 自动机(加强版) P3808 [模板]AC 自动机(简单版) 「JSOI2007 ...

  2. AC自动机学习笔记-2(Trie图&&last优化)

    我是连月更都做不到的蒟蒻博主QwQ 考虑到我太菜了,考完noip就要退役了,所以我决定还是把博客的倒数第二篇博客给写了,也算是填了一个坑吧.(最后一篇?当然是悲怆のnoip退役记啦QAQ) 所以我们今 ...

  3. AC自动机板子题/AC自动机学习笔记!

    想知道484每个萌新oier在最初知道AC自动机的时候都会理解为自动AC稽什么的,,,反正我记得我当初刚知道这个东西的时候,我以为是什么神仙东西,,,(好趴虽然确实是个对菜菜灵巧比较难理解的神仙知识点 ...

  4. AC 自动机学习笔记

    虽然 NOIp 原地爆炸了,目前进入 AFO 状态,但感觉省选还是要冲一把,所以现在又来开始颓字符串辣 首先先复习一个很早很早就学过但忘记的算法--自动 AC AC自动机. AC 自动机能够在 \(\ ...

  5. [AC自动机]【学习笔记】

    Keywords Search Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 131072/131072 K (Java/Others)To ...

  6. AC自动机学习笔记

    AC自动机 ----多个模板的字符串匹配 字典树Trie加上失配边构成 插入操作:ac.insert(p[i],i);构造失配函数:ac.getFail();计算文本串T中每个模板串的匹配数:ac.f ...

  7. 「AC自动机」学习笔记

    AC自动机(Aho-Corasick Automaton),虽然不能够帮你自动AC,但是真的还是非常神奇的一个数据结构.AC自动机用来处理多模式串匹配问题,可以看做是KMP(单模式串匹配问题)的升级版 ...

  8. [AC自动机][学习笔记]

    用途 AC自动机适用于一类用多个子串在模板串中匹配的字符串问题. 也就是说先给出一个模板串,然后给出一些子串.要求有多少个子串在这个模板串中出现过. KMP与trie树 其实AC自动机就是KMP与tr ...

  9. 算法笔记--字典树(trie 树)&& ac自动机 && 可持久化trie

    字典树 简介:字典树,又称单词查找树,Trie树,是一种树形结构,是哈希树的变种. 优点:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较. 性质:根节点不包含字符,除根节点外每一个 ...

随机推荐

  1. nginx&http 第四章 ngx http代理 && 转载

    Nginx访问上游服务器的流程大致分以下几个阶段:启动upstream.连接上游服务器.向上游发送请求.接收上游响应(包头/包体).结束请求. upstream相关的两个重要数据结构ngx_http_ ...

  2. 利用c++ std::getline实现split

    getline reads characters from an input stream and places them into a string: getline从输入流中读取字符, 并把它们转 ...

  3. 《.NET 5.0 背锅案》第6集-案发现场回顾:故障情况下 Kubernetes 的部署表现

    第1集:验证 .NET 5.0 正式版 docker 镜像问题 第2集:码中的小窟窿,背后的大坑,发现重要嫌犯 EnyimMemcachedCore 第3集-剧情反转:EnyimMemcachedCo ...

  4. 修改centos6启动动画(plymouth方式)

    centos6默认的启动动画是一个白蓝色的进度条,背景全黑色,现在需要对centos的启动动画进行定制 在查询了一些资料以后,发现有一个软件是可以对启动动画进行定制的,名字叫plymouth 这个在c ...

  5. JLC PCB 嘉立创自动确认生产稿,不讲武德?耗子尾汁!!!

    首先,开局一张图,嘉立创又不做人的一天.嘉立创不讲武德,耗子尾汁!!! 之前下单,勾选了确定生产稿和不加客编,结果生产稿出来还是给我加了客编.那我出10元的意思何在?让我自己花3元看我花的10元有没有 ...

  6. Python_科学计算平台__pypi体系的numpy、scipy、pandas、matplotlib库简介

    1.numpy--基础,以矩阵为基础的数学计算模块,纯数学 存储和处理大型矩阵. 这个是很基础的扩展,其余的扩展都是以此为基础. 快速学习入口 https://docs.scipy.org/doc/n ...

  7. 初识redis协议

    有关redis协议信息(https://redis.io/topics/protocol) 搭建环境 //jedis连接客户端 public class RedisClient { public st ...

  8. [i春秋]“百度杯”CTF比赛 十月场-Hash

    前言 涉及知识点:反序列化.代码执行.命令执行 题目来自:i春秋 hash  如果i春秋题目有问题可以登录榆林学院信息安全协会CTF平台使用 或者利用本文章提供的源码自主复现 [i春秋]"百 ...

  9. sqlilab less1-less10

    less-1 参数被单引号包裹,加单引号,闭合后绕过 less-2 参数没有被包裹,直接带入查询,不需要闭合 less-3 参数被 ('$id') 包裹,需要将他闭合 less-4 参数被小括号和双引 ...

  10. 关于Java里方法重载

    覆盖和重载很重要,并且比较容易混淆,所以面试中常见.基础回答:覆盖(Override),又叫重写,是指子类对父类方法的一种重写,方法名.参数列表必须相同,返回值小于父类,只能比父类抛出更少的异常,访问 ...