SAM有关

前言

本来是讲 trick 的,但是我想了又想觉得还是写成类学习笔记的样子吧,方便之后复习。反正就是不算 trick 但是和 trick 拥有同等地位的那种。

基础

定义

可接受一个串所有后缀的最小 DFA(注意是 DAG)。我们从根出发在 SAM 上走可以走出所有子串。

SAM 中有两个非常重要的东西,分别是 endpos 和 parent tree,但是长话短说。

endpos

对于 s 的一个子串 t 在 s 中所有结尾位置构成的集合就是这个串的 endpos。

然后讲一下性质。

  1. 假设 s 的两个子串 a 和 b 有 \(endpos(a)=endpos(b)\) 那么短串是长串的后缀,证明显然;否则两串的 endpos 无交集。
  2. 一个 endpos 等价类中所有字符串长度为公差为 1 的等差数列。

parent tree

设想我们在一个 s 的子串前面添加字符,其 endpos 会减小。我们可以通过添加不同字符使 endpos 被分成若干不交子集。这些关系可以用一棵树表示,此树即为 parent tree。树上每个点都对应一个 endpos 集合,而每个集合中有若干 s 的子串。

所以树从根节点往下走的时候,endpos 集合大小会变小但是所代表的子串长度会变长。并且注意每往下走都是在原来串的基础上在前面添加字符。所以反过来在树上跳父亲就是删去当前字符串的首位

考虑树的节点数。因为 endpos 集合每次划分都是一分为二所以有 \(2n-1\) 个点。

因为同一个 endpos 中串长连续,所以有 \(len_{fa}+1=minlen_u\)

构建

可以把 s 中的每个前缀代表的点连成的链当成树的主干,每次加点都是对主干的延伸。考虑每次加点会产生一些新的子串需要维护,考虑先维护长度为 2 的,然后对于长度更长的肯定能拆分成长度为 2 的和一段原来有的,并且对于一种字符 c 我们肯定不会在一个以它结尾的地方反复跳很多次(因为一次就能更新好)所以复杂度是线性的。

具体的,对于长度为 2 但是没有在 SAM 中的就直接连边,否则就判断是否需要添加新的字符串。然后将 endpos 切割并且更新上面的连边情况。

注意

删去前面的字符是在parent tree 上跳父亲而在后面添加字符是在SAM 上往下走

本质不同子串个数

两种思路,一种 dp 另一种根据性质。建议熟悉第一种思路熟练第二种代码,因为在 SAM 上的 dp 意识很有必要但是用第二种求法更快捷简便。

dp 做法:因为 SAM 上每条路径都对应一个子串,且 SAM 是 DAG,于是倒着 dp 即可。

通常做法:考虑每个点包含的子串,有 \(len_i-len{fa_i}\) 个,对每个点求和即可。

求一个串的出现次数

从根节点开始走,定位到目标点,答案是这个点的 endpos 集合大小。

如何求 endpos 大小?如果直接记录复杂度是假的,所以我们考虑优秀的做法。对于点 u 考虑到如果一个点 v 在其下方说明 \(endpos(v)\subset endpos(u)\)。于是就可以先求出 v 的大小(集合情况后面讲)。于是考虑一遍 dfs 子树求和即可。

最小表示法(感觉无用)

每次在 SAM 上走最小点走 \(|s|\) 次。

求给定串在区间的信息

先走到目标点 u,然后我们需要找到这个点的 endpos 中所有 \(x\in[l+len_u-1,r]\) 的信息然后进行操作之类。

现在考虑怎么搞。我们可以类似”求一个串的出现次数“中的做法,只是把维护大小改成用 ds 维护对应的信息。这时候就需要合并 ds 了。如果在线就要将不同的点记下来所以还要加可持久化。

求 SAM 上两点对应串的 lcs(suf)

由定义得到就是两点的 lca。

求一些串的 lcs(共)

为让复杂度更优秀,我们可以用最短串建 SAM。

对于一个串建 SAM,其他串在 SAM 上走。对于 SAM 上每个点记录 f 和 g 分别表示走到这个点时全局/当前的答案。可以在每次走的时候用一个变量如 res 维护当前的答案,然后更新 g(max),走完一遍再更新 f(min)。

最后在 f 中取 max。

tricks

因为做的题都太过于基础就先咕了。

参考

max的讲稿

posted @ 2025-04-30 19:49  Lyrella  阅读(35)  评论(1)    收藏  举报