【复习笔记】重习 AC 自动机
发现已经忘了许多。。。。于是复习一下
基础要点概况
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 自动机的更多相关文章
- 「笔记」AC 自动机
目录 写在前面 定义 引入 构造 暴力 字典图优化 匹配 在线 离线 复杂度 完整代码 例题 P3796 [模板]AC 自动机(加强版) P3808 [模板]AC 自动机(简单版) 「JSOI2007 ...
- AC自动机学习笔记-2(Trie图&&last优化)
我是连月更都做不到的蒟蒻博主QwQ 考虑到我太菜了,考完noip就要退役了,所以我决定还是把博客的倒数第二篇博客给写了,也算是填了一个坑吧.(最后一篇?当然是悲怆のnoip退役记啦QAQ) 所以我们今 ...
- AC自动机板子题/AC自动机学习笔记!
想知道484每个萌新oier在最初知道AC自动机的时候都会理解为自动AC稽什么的,,,反正我记得我当初刚知道这个东西的时候,我以为是什么神仙东西,,,(好趴虽然确实是个对菜菜灵巧比较难理解的神仙知识点 ...
- AC 自动机学习笔记
虽然 NOIp 原地爆炸了,目前进入 AFO 状态,但感觉省选还是要冲一把,所以现在又来开始颓字符串辣 首先先复习一个很早很早就学过但忘记的算法--自动 AC AC自动机. AC 自动机能够在 \(\ ...
- [AC自动机]【学习笔记】
Keywords Search Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 131072/131072 K (Java/Others)To ...
- AC自动机学习笔记
AC自动机 ----多个模板的字符串匹配 字典树Trie加上失配边构成 插入操作:ac.insert(p[i],i);构造失配函数:ac.getFail();计算文本串T中每个模板串的匹配数:ac.f ...
- 「AC自动机」学习笔记
AC自动机(Aho-Corasick Automaton),虽然不能够帮你自动AC,但是真的还是非常神奇的一个数据结构.AC自动机用来处理多模式串匹配问题,可以看做是KMP(单模式串匹配问题)的升级版 ...
- [AC自动机][学习笔记]
用途 AC自动机适用于一类用多个子串在模板串中匹配的字符串问题. 也就是说先给出一个模板串,然后给出一些子串.要求有多少个子串在这个模板串中出现过. KMP与trie树 其实AC自动机就是KMP与tr ...
- 算法笔记--字典树(trie 树)&& ac自动机 && 可持久化trie
字典树 简介:字典树,又称单词查找树,Trie树,是一种树形结构,是哈希树的变种. 优点:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较. 性质:根节点不包含字符,除根节点外每一个 ...
随机推荐
- select的限制
/*一.select实现并发服务器并发的两点限制 1.一个进能够打开的最大文件描述符限制.可以通过两种方式修改 ulimit -n :获取最大文件描述符个数 ulimit -n 2048:修改为204 ...
- day05-类型转换和变量
1.类型转换概念 java是强类型语言,所以有些运算的时候,需要用到类型转换 类型转换原则:低-->高,byte,short,char-->int-->long-->float ...
- Java初始化静态变量的时间顺序
1. 开始吧! 今天,我们来探讨交流下静态变量初始化过程.Java虚拟机在类加载期间也同样遵循这个过程. 2. 初始化过程 在较高的层次上,JVM执行以下步骤: 首先,加载并链接类.然后,这个过程的& ...
- 定位一个网络问题引起的ceph异常
前言 有一个ceph环境出现了异常,状态就是恢复异常的慢,但是所有数据又都在走,只是非常的慢,本篇将记录探测出问题的过程,以便以后处理类似的问题有个思路 处理过程 问题的现象是恢复的很慢,但是除此以外 ...
- Node.js 爬虫爬取电影信息
Node.js 爬虫爬取电影信息 我的CSDN地址:https://blog.csdn.net/weixin_45580251/article/details/107669713 爬取的是1905电影 ...
- 这个厉害了,ssm框架整合全过程,建议收藏起来好好看看
1.0 环境要求 IDEA MySQL 5.7.19 Tomcat 9 Maven 3.6 1.1 数据库 创建书籍数据库表,包括书籍编号,书籍名称,书籍数量以及书籍描述. CREATE DATABA ...
- 通过Camtasia来制作画中画视频效果的方法
随着全民娱乐化的发展,视频的形式也更加多种多样了.视频形式的多样化能让观众从不同形式的视频中观赏到更有趣味的内容.比如像画中画的视频形式,让视频中的人物看起来像与观众一同观看视频,或者形成两个视频的对 ...
- HTML常用标签总结 [建议收藏]
好好学习,天天向上 本文已收录至我的Github仓库DayDayUP:github.com/RobodLee/DayDayUP,欢迎Star 1. 标题标签 <h1> </h1> ...
- js 手机号验证
1 js 通过正则表达式对手机号进行验证 2 3 var reg = /^1[3|4|5|8][0-9]\d{4,8}$/; 4 var sMobile = document.mobileform.m ...
- yum安装软件时报错libmysqlclient.so.18()(64bit)
错误信息 yum -y install sysbench 安装sysbench提示缺少依赖包如下图: 主要原因 缺少Percona-XtraDB-Cluster-shared-55-5.5.37-25 ...