字符串笔记

标注 的是简单题或者模板题。

标注 的是常规的应用。

标注 的是难题或者好题。

ACAM#

首先总结一下 AC 自动机配合 fail 树完成的最常见的几种操作。

求字符串 sx 在字符串 sy 内出现了几次。

  1. sx 在 AC 自动机上对应的节点标记出来,对于每个 sy 的前缀对应的节点,求其 fail 树上的祖先是否有被标记的,可以转化为子树加,单点查询。
  2. sy 的所有前缀在 AC 自动机上对应的节点标记出来,求 sx 对应的节点的子树内有几个标记即可。

求集合 T 内有多少字符串包含了集合 S

  • 见下文 P5840 [COCI2015] Divljak 一题。
P4052 [JSOI2007] 文本生成器

存在至少一个套路的容斥,变成不存在,也就是不能到 AC 自动机上的某些节点,直接 DP 即可。

si 对应的节点为 p 则在 fail 树上 p 这个子树都是不能到达的节点,因为 fail 树上一个点包含这个点的祖先(精确的说是祖先为这个点的后缀)。

P2414 [NOI2011] 阿狸的打字机

首先建出 Trie 树,虽然 |si| 可能很大,但是由于题目中的生成方式 Trie 上的节点数量仍然是 O(n) 的。

然后遇到上文提到的经典问题:求 sxsy 内出现的次数。

对于多次询问呢,考虑离线下来,枚举这个 y,然后对于所有 x 计算子树和即可。

相同的应用 CF1207G - Indie Album

这个题可以有两种建 AC 自动机的方法,值得一提的是,如果你选择对 s 建立 AC 自动机,需要把 t 中的字符串也加入其中,否则会出现询问串不存在导致的问题。

P7456 [CERC2018] The ABCD Murderer

他同时允许剪出的单词互相重叠,只需要重叠部分相同。

这是题目的关键,这允许对于一个点,只使用最长的以这个点结尾的那块,称其为 Li

那么对于 s[1,i] 这个前缀在 AC 自动机上对应的点 pLi 就是到根路径上的最大 L,直接递推即可,然后使用线段树优化 DP。

P5840 [COCI2015] Divljak

首先对 s 建立 ACAM。

那么对于 T 中的一个字符串 ti,要做的事情是将 ti 所有前缀对应的节点 p 到根路径的加一,比较无脑的是线段树合并,但是这里介绍另一种方法。

将所有点按照 dfn 序排序,我们在加完之后对相邻两个的 LCA 减即可容斥掉多的情况,这个也是经典技巧。

P5231 [JSOI2012] 玄武密码

s 的子串这个东西直接拿所有前缀的所有后缀来刻画,也就是 fail 树上的若干到根路径的并,直接 dfs 即可。

然后对于每个串的查询就是简单的了。

CF696D - Legen...

直接对 T 进行 DP,发现 DP 中的过程是一样的,那么直接矩阵乘法优化即可。

CF547E - Mike and Friends

差分询问,变成求 sxs1si 的出现次数。

从前往后扫描,把询问挂在 i 上,然后直接计算。

CF1202E - You Are Given Some Strings...

考虑计算 fi 表示有多少字符串是 t[1,i] 的后缀,gi 表示有多少字符串是 t[i,n] 的前缀。直接用 AC 自动机求出来,然后计算答案。

P8147 [JRKSJ R4] Salieri

首先二分答案 ans,转化成求排名。

每次建立大小为 |S| 的虚树,查询的时候虚树的一条边对应原树的一条链,这些 cnt 都是相同的,只需要查询 wanscnt 的个数就好了,这个直接主席树。

CF1483F - Exam

将串按长度排序,那么对于一个串去计算答案,考虑枚举右端点,那么左端点必须是极左的,这也说明了答案是 O(n) 级别的。

那么首先这些可能的区间是不能有包含的,先判断一下。

然后发现对于 AC 自动机上的一个结点,这个字符串能成为答案,当且仅当其被某个区间包含,又不被区间的子区间包含,这个等价与其被计算到的次数(fail 树上)等于出现的次数。

PAM#

CF932G - Palindrome Partition

此题和 CF906E - Reverses 是相同的思路和做法。

每次在左右寻找?将序列从中间断开,右边的部分折叠过来,那么能选择一段必然有正反序列互为逆序。

重新构建这个序列,原来是 s1s2s3t1t2t3 现在是 s1t1s2t2s3t3

那么条件变成了一段是偶回文串才能转移,直接建立 PAM,同时进行 DP。

现在的问题是每次暴力跳 fail 来得到所有回文后缀的复杂度是 O(n2) 的。

需要进一步挖掘性质,考虑回文后缀是原串的 Border,那么根据一个串的 Border 可以被划分为 O(logn) 个连续段,使得每个段都是一个等差数列,同样可以对 fail 树做这样的划分,记录一个 link 变量即可。

现在的转移是 fi=jfj[s[j+1,i] is palindromic],直接根据等差数列还是做不了。

那么我们额外记录一个 gi 表示 ilink(i) 的路径上点的 f 之和,也就是 filen,filen+d,filen+2d,

考虑加入一个点对答案的贡献。

这种 Border 的性质是 ACAM 所不具备的。

img

左端点只多了一个,那么直接维护就好了(图片来自 @zhylj)。

SA#

比 SAM 简单而同样有力的工具,核心在于对每个后缀按照排序,得到 sa[i] 表示排名为 i 的后缀的起始位置,和 rk[i] 表示 i 这个后缀的排名。

通过倍增的方法构造,一开始比较后缀的前 1 位,然后是前 2 位,前 4 位等等,每次用之前得到的信息进行双关键字排序。

代码实现如下:

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。

首先定义 endpos(s) 表示字符串 s 出现的结束位置的集合。

在 SAM 中,endpos 集合相同的点属于同一个结点,每次通过后缀链接来动态构建。

从任意一个点出发,到达终止节点的路径构成了字符串的所有后缀,而过程中得到了所有后缀的所有前缀,相当于是所有子串,也就是说这是能表示子串信息的一种结构,区别于 AC 自动机,SAM 是一种单串的结构,可以更详细的表示所有子串,AC 自动机能表示的子串仅限于能匹配的子串。

本题需要求解每个结点对应 endpos 集合的大小,首先建出 parent 树,然后对于原串所有前缀对应的节点,size 初始化为 1,然后子树的 size 之和就是该结点的 endpos 集合大小。

P3975 [TJOI2015] 弦论

求解第 k 小的子串,可以利用类似线段树上二分的思想,一位位的确定下去,这样我们只需要知道 DAG 上一个点向后的路径数量,记为 sumi

对于要求本质不同子串的情况,sumi=sizei,否则为 1,跑一个拓扑排序即可,实际上可以将点按照 len 排序,这样可以不用真的写拓扑排序,以及之前算 size 也是一样。

SP1811 LCS - Longest Common Substring

SAM 也可以当 ACAM 使用,用来做一些匹配问题。

对于 s 建立 SAM,尝试计算 t 每个前缀和 s 的最长公共子串,记录一个 len 表示答案。

如果当前点不能匹配,那么一直跳 fail,否则就 lenlen+1

这样的时间复杂度是 O(n) 的,因为 len 每次跳 fail 就会减少,减少复杂度不超过增加的,增加是 O(n) 的,因此总复杂度也是。

对于k 个字符串的情况,也可以类似的做。

P6640 [BJOI2020] 封印

t 建立 SAM,然后按照上题的做法得到一个 vali 表示以 i 结尾的最长公共子串。

查询的时候二分答案,那么只需要一个 ST 表查询区间最大值即可,这种套路是经常出现的。

作者:紊莫

出处:https://www.cnblogs.com/wenmoor/p/18612480

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   紊莫  阅读(9)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示
more_horiz
keyboard_arrow_up light_mode palette
选择主题
menu