【笔记】字符串选讲:ACAM、SAM 2024.8.1 & 2025.8.1

【笔记】字符串选讲:ACAM、SAM 2024.8.1 & 2025.8.1

[COCI2015-2016#5] OOP(Trie)

P6727 [COCI2015-2016#5] OOP - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

正反串分别建 Trie,可以搞出两个 dfn 区间,加之长度限制,三维数点。

\(O(n\log n)\) 做法。将字典串 \(S[1..m]\),对所有 \(1\leq i\leq m\),将 \(S[i+1,m]\) 的 hash 值插入到 \(S[1,i]\) 这个字符串在 Trie 树上的点。这样,询问就变成了,先找到通配符前的部分在 Trie 树上的位置,然后在子树中找有多少个 hash 值与通配符后的部分相等。离线所有询问,仅需一个 map。

[Guilin21H] Popcount Words(ACAM、倍增)

Popcount Words - Problem - QOJ.ac

对询问串建 AC 自动机。观察到结构有倍增性质,直接倍增。

\[S[0, 2^k)=S[0, 2^{k-1})+S'[0, 2^{k-1}) \]

\(S'\) 表示所有字符 \(0\)\(1\)\(1\)\(0\)。可以倍增求出每个 \(S[0, 2^k)\) 在 AC 自动机上的位置。我们可以将大串在线段树上拆成 \(O(n\log n)\) 个小串,小串不断倍增将经过次数传递下去,传递到最后一层就是答案。

无标题(ACAM)

有关系但不多的题目:P5310 [Ynoi2011] 遥远的过去 - 洛谷

  • 称两个序列 \(a_1,\cdots,a_m\)\(b_1,\cdots,b_m\) 为本质相同的当且仅当每个序列内部不包含相同的数,且对任意 \(i,j\),有 \([a_i<a_j]=[b_i<b_j]\)
  • 给定⼀个 \(1\)\(n\) 的排列 \(p\)\(q\) 次查询,每次查询⼀个 \(1\sim m_i\) 的排列,问有多少个 \(p\) 的区间和该小排列本质相同。
  • \(n, \sum m_i\leq 2\times 10^5\)

将排列 \(a[1..n]\) 改写为一个整数序列 \(f(a[1..n])\),定义为 \(f(a[1..n])_i=\sum_{j<i}[a_j>a_i]\)(某种康托展开)。那么两个排列本质相同就是 apply \(f\) 之后相等。

对询问排列 apply \(f\) 后建 AC 自动机,但是这个 AC 自动机的 fail 有讲究,每个点的出边是在这个点对应的排列中的排名,可能跳了一次 fail 之后排名会因为一段前缀被砍而骤降(排名是排列最后一个数在该区间的排名)。

总结一下,我们要做的事情:在节点 \(u\) 上,已知它和它的祖先们的所有 fail 指针,求它接上一个数(不是排名)后的 fail 指针(或者会跳到的地方)。做法是:从 \(u\) 开始遍历 \(u\) 的所有 fail 树祖先,对当前节点求它接上这个数之后这个数的排名,如果这个排名存在对应的出边,则连过去,否则继续遍历祖先。只需要判断最后一个数的排名是因为这是 fail 树,前面的部分由 fail 树性质保证相等。

这里暴力跳 fail 的复杂度在给定多个串时是对的,给定 Trie 树是不对的。许庭强说他不知道这是为什么。

[CF1801G] A task for substrings(ACAM)

A task for substrings - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

惊人的一步:\(ans[l,r]=ans[1,r] - ans[1,l-1] - somesubstr\),其中 \(somesubstr\) 是所有 \(x<l\leq y\leq r\) 的出现在模式串中的 \(t[x,y]\) 的个数。这玩意竟然是可以做的,考虑令 \(A=t[x,l-1], B=t[l,y]\),那么实际上可能的 \((A,B)\) 也就 \(\sum\) 模式串串长这么多,所以可以转化为二维数点,变成 \(t[1,l-1]\) 有后缀是 \(A\)\(t[l,r]\) 有前缀是 \(B\),也就是有两对子树关系,二维数点。是不是需要一些匹配技巧?

\(A\)\(t[1,l-1]\) 的后缀:对所有正的模式串建 ACAM,则所有 \(A\) 都是 ACAM 的一个点。将 \(t[1,l-1]\) 在 ACAM 上跑,则 \(A\) 是其 fail 树上祖先。

\(B\) 的限制则是在反串 Trie 上的,顺带附赠一个倍增(从 \(t[l,n]\) 对应节点开始,跳一些 fail,使长度 \(\leq r-l+1\))。

改成 \((A, B)\) 对其子树贡献,所以就是矩形加,单点查询的二维数点问题。

[Nanjing23J] Suffix Structure(ACAM)

Suffix Structure - Problem - QOJ.ac

使用可持久化线段树维护 Trie 图,建立 AC 自动机。

对除了根以外的每个节点 \(u\),首先二分哈希(需要跳 fail 等价于接出来的串不在 Trie 上)求出它第一次跳 fail 的时间 \(l\)(前 \(l\) 个字符接上之后都在 AC 自动机上),和它跳到的 fail \(v\)。除非它不需要跳 fail,否则:

  1. 如果 \(v\) 不是根,则 \(f(u,i\leq l)=d(u)+i, f(u,i>l)=f(v,i)\),我们可以让 \(f(v,i)\) 多计算一次,然后 \(g(i\leq l){}^+\gets d(u)-d(v)\)(注意由于 \(v\) 不是根所以 \(f(v,i\leq l)=d(v)+i\))。
  2. 否则 \(v\) 是根,\(f(v,i\leq l)=d(v)+i\) 就没有了,但我们仍然可以这样做,让 \(f(rt,i)\) 多计算一次,然后 \(g(i\leq l){}^-\gets f(rt,i)\),再 \(g(i\leq l){}^+\gets d(u)+i\),这是和刚才的操作差不多的,只是其中的 \(f(rt,i)\) 暴力计算贡献了。

对每个点记录贡献系数 \(a_u\) 并递推,做很多差分就可以了。很多差分,很多很多差分。

\(a{}^+\gets b\) 表示 a += b。因为 \(+=\) 太丑了。

[Hangzhou22L] Levenshtein Distance(编辑距离)

Levenshtein Distance - Problem - QOJ.ac

改为求 \(\min(k, \text{s与t的编辑距离})\)\(k\leq 5000, |s|, |t|\leq 10^5\)。这是上世纪的论文题,听说甚至是 Tarjan 先生的论文。更正了,是新世纪初 2002 年的论文。

显然

\[d_{i, j}=\min(d_{i-1,j-1}+[s_i\neq t_j], d_{i-1, j}+1, d_{i, j-1}+1) \]

必然有

\[d_{i, j}\geq |i-j| \]

根据归纳可以发现:

\[d_{i-1, j-1}\leq d_{i, j} \]

即对角线单调不降,启发沿着对角线转移。

\(f_{v, t}\) 表示最大的 \(x\) 使得 \(i-j=t, d_{i, j}=v\)。转移考虑先走第二 / 三种转移,然后沿着对角线一路右上,走到了 \(f_{v+1, t\pm 1}\)。注意到后者是两个后缀的最长公共前缀问题,可以建立后缀数组以 \(O(1)\) 转移。因为状态数是 \(O(k^2)\) 的,所以总复杂度 \(O(k^2+n\log n)\)

前缀本质不同子串个数(SAM)

建 SAM,\(i\)\(1\) 枚举到 \(i\),尝试求出增量,从 \(S[1,i]\) 往上跳,一边跳一边打标记,直到遇到打过标记的点停止。显然正确。

[NOI2018] 你的名字(SAM)

P4770 [NOI2018] 你的名字 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

全局的情况。可以将 \(T\)\(S\) 的 SAM 上跑,对每个 \(r\) 求出最小的 \(l\) 使得 \(T[l,r]\in S\)。之后,对 \(T\) 建 SAM,将这些非法的 \(T[l,r]\) 标记出来,复杂度显然是 \(O(|T|)\)

区间的情况。即我们只需要多次判断 \(S[l,r]\) 是否在 \(S[L,R]\) 中,即查询 \(S[l,r]\) 对应的等价类中 \(\geq L+r-l\) 的最小的 endpos 是否 \(\leq R\)。线段树合并即可。注意这里需要可持久化所以只能写线段树合并。注意这里的匹配部分需要改一下,有可能出现在当前等价类中减少匹配长度就能匹配上的情况(因为我们忽略了一些潜在的 endpos)。

[BJWC2018] Border 的四种求法(SAM)

P4482 [BJWC2018] Border 的四种求法 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

即求一个最大的 \(x\)\(S[l,x]=S[r-x+l,r]\implies LCS(S[1,x], S[1,r])\geq x-l+1\)

建后缀树。从右往左扫,扫到 \(r\) 加入询问,扫到 \(x\) 尝试解决之前的问题。核心是重链剖分,将询问放在每条重链上等待解决,在跳到重链的那个点打上一些标记,可以发现 \(x\)\(r\) 打的标记点总有一个是我们需要的 LCA。于是就是分讨谁是 LCA,在不等式上合并同类项,线段树维护。

注意为什么要从右往左扫,因为判定条件是 \(f(x)\leq g(r)\),如果从左往右扫会多一个 \(\log n\) 用于树套树(因为要求 \(x\) 是最大的)。那现在我们去扫 \(x\) 的时候,就要把所有符合它条件的询问直接删掉,并记录答案。

结论:一个串的 border 可以被分为 \(O(\log n)\) 段等差数列。

区间本质不同子串个数(SAM)

P6292 区间本质不同子串个数 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

从前缀本质不同子串个数出发。链上的子串,将其答案记在左端点上。我们现在想要加入一条链。发现长度连续,endpos 相同的子串,可以 \(O(1)\) 次线段树操作一次解决。我们想干的事情就是:

  1. 将这条链划分为很多段,每一段的上次出现右端点(记作 \(lst\))相同;
  2. 整条链的 \(lst\) 推平为一个数。

\(lst\) 相同的段看作一条实链,这是 LCT 的 access 操作。\(O(n\log^2n)\)

*[ECFinal22B] Binary String

Binary String - Problem - QOJ.ac

划分为若干个 \(1\) 的连续段和 \(0\) 的连续段,每次操作就是 \(1\) 段右移,\(0\) 段左移,长度为 \(1\) 的段是反的。最终如果 \(0\)\(1\) 多,会出现很多个 \(1\) 全部独立一段,然后出现循环。所以我们需要求出什么时候“会出现很多个 \(1\) 全部独立一段”,再求那个时候的循环节。可以找一个点断开,使得前缀和 \(\geq 0\),后面怎么做不会。

[CCPCF22G] Recover the String

Recover the String - Problem - QOJ.ac

特判一条链代表的全 a 串。首先可以做拓扑排序,求出每个点代表的长度。从最长串长度开始,我们尝试去寻找长度为某个值的区间分别对应哪些点。除了 \([1,n]\) 的第一次区分可以随意决定,其它的情况:我们现在要知道 \([l,r]\) 对应哪个点,那我们就去看 \([l-1,r],[l,r+1]\) 对应的点的共同入边,它就是 \([l,r]\) 代表的点;有可能会出现共同入边有很多的情况,如果有一个共同入边只有一条,我们就确定它,这样就能确定别的点;如果所有点共同入边都很多,说明这个串是 ababababa...,直接击杀即可。这样就是 \(O(|S|^2)\)

优化就是用并查集,同长度的所有相同的子串,将它们的右端点合并起来。然后去传递到下一层,和刚才一样。

CF1098F Ж-function

即求

\[\sum_{i=l}^r\min(LCP(i, l), r - i + 1) \]

这里有三维,你强行推一下就会有 \(O(n\log ^3n)\) 的做法。尝试拆式子差分掉一维就能变成 \(O(n\log^2n)\)。具体过程不记了。

[候选队互测 2022] 可爱多的字符串

\[\sum_{p=l}^r\left(S[\min(p+LCP(p, l)-1, r)]-S[p-1]\right) \]

\(S\)\(w\) 的后缀和。\(w_i=1\) 和 CF1098F 是一样的。所以这和上题的做法也是一样的(树剖线段树枚举 LCA)。唯一做不了的一个部分:

\[\sum_{p\in\text{某个点的子树}}S[p+len_v-1] \]

\(v\) 表示从 \(l\) 跳上来的重链点。这个的做法是把它搬到与它对称的后缀树上去做。具体来说,当 \(len_v\)\(LCP\) 时,就有 \(s[p..p+len_v-1]\)\(s[l..n]\)前缀,也就是 \(s[1..l+len_v-1]\)后缀。这样就成功转置了,\(p+len_v-1\) 成为一个 endpos,我们维护 endpos 和 \(p\) 的线段树合并,可能就可以了。

注意!树剖线段树的做法,要小心询问点和信息点汇合的重链不止一条,我们只需要在第一次汇合的重链上统计,其它地方要把相应的贡献删掉。

[CCPCF2024A] Requiem for Qingyu / 青鱼安魂曲

改成后缀并写出后缀树。在一个 SAM 节点上,我们会有一些 endpos,还有一些未被覆盖的位置集合(显然所有位置都被经过的子串覆盖了一次就合法)。我们的想法是用线段树维护 endpos 和覆盖情况,用线段树分裂操作把它们传下去,在每个节点上暴力更新覆盖情况并尝试统计答案。

线段树中,一个 endpos 维护上一个 endpos 右边的第一个未覆盖位置或记录不存在。这样我们就可以暴力覆盖了。向子节点下传时,将属于该子树的 endpos 划分出去(这个可以用区间 LCA 判定),并相应下传 endpos 上维护的未覆盖位置,这样就行了。

复杂度是 \(O(n\log ^2n)\)。线段树分裂的复杂度是 \(O(n\log n)\)。分裂多出来的“未覆盖位置”只有 \(O(n\log n)\) 个,因为只有相邻两个 endpos 不同时会多出来,这个的个数可以用启发式合并分析。

*QOJ4882 String Strange Sum

和上一题一样。这真的是可以计算的!

ZROI2502

区间本质不同子串的 occ 的和,其中一个子串的 occ 是它在整个串中的出现次数。

咕咕咕

posted @ 2024-08-01 14:37  caijianhong  阅读(226)  评论(0)    收藏  举报