字符串笔记
标注 的是简单题或者模板题。
标注 的是常规的应用。
标注 的是难题或者好题。
ACAM#
首先总结一下 AC 自动机配合 fail 树完成的最常见的几种操作。
求字符串 在字符串 内出现了几次。
- 将 在 AC 自动机上对应的节点标记出来,对于每个 的前缀对应的节点,求其 fail 树上的祖先是否有被标记的,可以转化为子树加,单点查询。
- 将 的所有前缀在 AC 自动机上对应的节点标记出来,求 对应的节点的子树内有几个标记即可。
求集合 内有多少字符串包含了集合 。
- 见下文 P5840 [COCI2015] Divljak 一题。
P4052 [JSOI2007] 文本生成器
存在至少一个套路的容斥,变成不存在,也就是不能到 AC 自动机上的某些节点,直接 DP 即可。
设 对应的节点为 则在 fail 树上 这个子树都是不能到达的节点,因为 fail 树上一个点包含这个点的祖先(精确的说是祖先为这个点的后缀)。
P2414 [NOI2011] 阿狸的打字机
首先建出 Trie 树,虽然 可能很大,但是由于题目中的生成方式 Trie 上的节点数量仍然是 的。
然后遇到上文提到的经典问题:求 在 内出现的次数。
对于多次询问呢,考虑离线下来,枚举这个 ,然后对于所有 计算子树和即可。
相同的应用 CF1207G - Indie Album
这个题可以有两种建 AC 自动机的方法,值得一提的是,如果你选择对 建立 AC 自动机,需要把 中的字符串也加入其中,否则会出现询问串不存在导致的问题。
P7456 [CERC2018] The ABCD Murderer
他同时允许剪出的单词互相重叠,只需要重叠部分相同。
这是题目的关键,这允许对于一个点,只使用最长的以这个点结尾的那块,称其为 。
那么对于 这个前缀在 AC 自动机上对应的点 , 就是到根路径上的最大 ,直接递推即可,然后使用线段树优化 DP。
P5840 [COCI2015] Divljak
首先对 建立 ACAM。
那么对于 中的一个字符串 ,要做的事情是将 所有前缀对应的节点 到根路径的并加一,比较无脑的是线段树合并,但是这里介绍另一种方法。
将所有点按照 dfn 序排序,我们在加完之后对相邻两个的 LCA 减即可容斥掉多的情况,这个也是经典技巧。
P5231 [JSOI2012] 玄武密码
的子串这个东西直接拿所有前缀的所有后缀来刻画,也就是 fail 树上的若干到根路径的并,直接 dfs 即可。
然后对于每个串的查询就是简单的了。
CF696D - Legen...
直接对 进行 DP,发现 DP 中的过程是一样的,那么直接矩阵乘法优化即可。
CF547E - Mike and Friends
差分询问,变成求 在 的出现次数。
从前往后扫描,把询问挂在 上,然后直接计算。
CF1202E - You Are Given Some Strings...
考虑计算 表示有多少字符串是 的后缀, 表示有多少字符串是 的前缀。直接用 AC 自动机求出来,然后计算答案。
P8147 [JRKSJ R4] Salieri
首先二分答案 ,转化成求排名。
每次建立大小为 的虚树,查询的时候虚树的一条边对应原树的一条链,这些 cnt 都是相同的,只需要查询 的个数就好了,这个直接主席树。
CF1483F - Exam
将串按长度排序,那么对于一个串去计算答案,考虑枚举右端点,那么左端点必须是极左的,这也说明了答案是 级别的。
那么首先这些可能的区间是不能有包含的,先判断一下。
然后发现对于 AC 自动机上的一个结点,这个字符串能成为答案,当且仅当其被某个区间包含,又不被区间的子区间包含,这个等价与其被计算到的次数(fail 树上)等于出现的次数。
PAM#
CF932G - Palindrome Partition
此题和 CF906E - Reverses 是相同的思路和做法。
每次在左右寻找?将序列从中间断开,右边的部分折叠过来,那么能选择一段必然有正反序列互为逆序。
重新构建这个序列,原来是 和 现在是 。
那么条件变成了一段是偶回文串才能转移,直接建立 PAM,同时进行 DP。
现在的问题是每次暴力跳 fail 来得到所有回文后缀的复杂度是 的。
需要进一步挖掘性质,考虑回文后缀是原串的 Border,那么根据一个串的 Border 可以被划分为 个连续段,使得每个段都是一个等差数列,同样可以对 fail 树做这样的划分,记录一个 link 变量即可。
现在的转移是 ,直接根据等差数列还是做不了。
那么我们额外记录一个 表示 到 的路径上点的 之和,也就是 。
考虑加入一个点对答案的贡献。
这种 Border 的性质是 ACAM 所不具备的。
左端点只多了一个,那么直接维护就好了(图片来自 @zhylj)。
SA#
比 SAM 简单而同样有力的工具,核心在于对每个后缀按照排序,得到 sa[i]
表示排名为 的后缀的起始位置,和 rk[i]
表示 这个后缀的排名。
通过倍增的方法构造,一开始比较后缀的前 位,然后是前 位,前 位等等,每次用之前得到的信息进行双关键字排序。
代码实现如下:
int sa[N], rk[N], od[N], id[N], cnt[N];
int n, m = 128 /*字符集大小*/, p = 0;
for (int i = 1; i <= n; i++) cnt[rk[i] = s[i]]++;
for (int i = 1; i <= m; i++) cnt[i] += cnt[i - 1];
for (int i = n; i >= 1; i--) sa[cnt[rk[i]]--] = i;
for (int w = 1; p != n; w *= 2, m = p) {
int cur = 0;
for (int i = n - w + 1; i <= n; i++) id[++cur] = i;
for (int i = 1; i <= n; i++) if (sa[i] > w) id[++cur] = sa[i] - w;
for (int i = 1; i <= m; i++) cnt[i] = 0;
for (int i = 1; i <= n; i++) cnt[rk[i]]++;
for (int i = 1; i <= m; i++) cnt[i] += cnt[i - 1];
for (int i = n; i >= 1; i--) sa[cnt[rk[id[i]]]--] = id[i];
memcpy(od, rk, sizeof rk), p = 0;
for (int i = 1; i <= n; i++) {
if (od[sa[i]] == od[sa[i - 1]] &&
od[sa[i] + w] == od[sa[i - 1] + w])
rk[sa[i]] = p;
else rk[sa[i]] = ++p;
}
}
还有比较重要的 height 数组,求法如下:
for (int i = 1, k = 0; i <= n; i ++) {
if (rk[i] == 1) continue; if (k) k--;
while (s[i + k] == s[sa[rk[i] - 1] + k]) k++;
h[rk[i]] = k;
}
SAM#
P3804 【模板】后缀自动机(SAM)
简述一下 SAM。
首先定义 表示字符串 出现的结束位置的集合。
在 SAM 中, 集合相同的点属于同一个结点,每次通过后缀链接来动态构建。
从任意一个点出发,到达终止节点的路径构成了字符串的所有后缀,而过程中得到了所有后缀的所有前缀,相当于是所有子串,也就是说这是能表示子串信息的一种结构,区别于 AC 自动机,SAM 是一种单串的结构,可以更详细的表示所有子串,AC 自动机能表示的子串仅限于能匹配的子串。
本题需要求解每个结点对应 集合的大小,首先建出 parent 树,然后对于原串所有前缀对应的节点, 初始化为 ,然后子树的 之和就是该结点的 集合大小。
P3975 [TJOI2015] 弦论
求解第 小的子串,可以利用类似线段树上二分的思想,一位位的确定下去,这样我们只需要知道 DAG 上一个点向后的路径数量,记为 。
对于不要求本质不同子串的情况,,否则为 ,跑一个拓扑排序即可,实际上可以将点按照 排序,这样可以不用真的写拓扑排序,以及之前算 也是一样。
SP1811 LCS - Longest Common Substring
SAM 也可以当 ACAM 使用,用来做一些匹配问题。
对于 建立 SAM,尝试计算 每个前缀和 的最长公共子串,记录一个 表示答案。
如果当前点不能匹配,那么一直跳 ,否则就 。
这样的时间复杂度是 的,因为 每次跳 就会减少,减少复杂度不超过增加的,增加是 的,因此总复杂度也是。
对于有 个字符串的情况,也可以类似的做。
P6640 [BJOI2020] 封印
对 建立 SAM,然后按照上题的做法得到一个 表示以 结尾的最长公共子串。
查询的时候二分答案,那么只需要一个 ST 表查询区间最大值即可,这种套路是经常出现的。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!