【复习笔记】重习 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树,是一种树形结构,是哈希树的变种. 优点:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较. 性质:根节点不包含字符,除根节点外每一个 ...
随机推荐
- 运维和shell
什么是运维 术语名词 IDC--(Internet Data Center)互联网数据中心,主要服务包括整机租用.服务器托管.机柜租用.机房租用.专线接入和网络管理服务等.广义上的IDC业务,实际上就 ...
- Design Principle vs Design Pattern 设计原则 vs 设计模式
Design Principle vs Design Pattern设计原则 vs 设计模式 来源:https://www.tutorialsteacher.com/articles/differen ...
- DP中环形处理 +(POJ 1179 题解)
DP中环形处理 对于DP中存在环的情况,大致有两种处理的方法: 对于很多的区间DP来说,很常见的方法就是把原来的环从任意两点断开(注意并不是直接删掉这条边),在复制一条一模一样的链在这条链的后方,当做 ...
- TCP的ACK机制
下面是整个的tcp的三次握手和四次挥手的协议 TCP四次挥手 在客户端先发送一个FIN的包,表示要close(),客户端想和连接断开,发完之后出于FIN_WAIT_1状态下:服务端收到之后就变成CLO ...
- 如何使用Camtasia制作动态动画场景?
也许在学习编辑视频的你知道Camtasia 2019(win系统),知道Camtasia的视频编辑功能,录制屏幕功能,但你可能想不到,Camtasia还可以制作动态动画场景.跟我一起学习一下吧! 一. ...
- 教您使用OCR编辑器复制文档内容
ABBYY FineReader 15允许用户复制图像或者扫描页面上的内容,可复制其中的文本.图片和表格的信息.在复制过程中,用户无需将图像或扫描页面转换为可编辑的格式,可以直接在ABBYY Fine ...
- guitar pro系列教程(十五):Guitar Pro播放没有声音怎么办?
相信很多朋友在安装了guitar pro打谱后都会发现没有声音,关于这个问题相信大家都很苦恼到底是什么问题呢,今天小编就这个问题与大家好好讨论讨论,希望对大家有帮助,感兴趣的朋友可以进来一起学习哦. ...
- Logstash使用mongodb插件报错: ArgumentError: wrong number of arguments (given 2, expected 1)
目录 背景 安装插件过程 背景 今天在使用logstash收集日志存储到mongodb的安装过程遇到了个错误,记录下来,错误就是下面这样: 配置文件很简单,由于是测试环境,命令行传入日志输入由ruby ...
- CF1156D 0-1-Tree
路径考虑顺序. 显然合法的路径只有以下两种: 一段 \(0\) 加一段 \(1\) 或一段 \(1\) 加一段 \(0\). 全 \(0\) 或全 \(1\). 用并查集将边权为 \(0\) 和 \( ...
- CCF-CSP认证 C++题解目录
持续更新中,记录刷题过程并分享一下小小的心得总结. 试题编号 试题名称 标签 202006-1 线性分类器 | 题解 线性规划 202006-2 稀疏向量| 题解 归并排序思想 202006-3 化学 ...