SAM 及一些扩展
理解很浅,而且掺杂大量感性理解。
主要来自 max 的课,稍微抄了一点课件(可以吗?)。也参考了一些博客。
SAM
这里主要记录一些使用 SAM 的基本技巧。
SAM 用于维护所有子串。
SAM 的结构可以看成两部分:DAWG 和 parent tree。
SAM 的结构不对称。
DAWG 是反串 parent tree 压缩后的结构,压缩方式为将相同子树合并。(?)
一般注意事项
多测清空和仅仅初始化不同。
不要忘了写 p=fa[p](两处)。
写结构体就把东西都写在结构体里。
空间开够。注意 \(n=1\) 时结点数并非 \(2n-1\),不知道还有没有特例。
构建时分裂结点的时候信息要不要复制或做一些处理之类的问题。建议先构建好 SAM,再走前缀结点赋这些信息,而非边构建边处理。
构建中的分裂与 nq
(P6152 [集训队作业2018] 后缀树节点数)
此题的一个重点在于对结点 nq 的理解:建立它的充要条件是 q 的分裂,而 q 的分裂意味着 nq 的 endpos 和 新的 q 的 endpos 不同,那么一定会出现两个位置 \(x,y\),使得 q 中最长串出现在以 \(x,y\) 开头的位置,满足 \(S_{x-1}\neq S_{y-1}\)(这是必要条件)。
对串操作
-
DAWG 上走一条边表示在结尾添加一个字符,parent tree 上走一条边表示在开头添加一个字符。反之就是在开头结尾删除字符。
-
可以在 parent tree 上处理出边上的字符,表示从当前点最长的串到儿子的最短的串需要在开头添加的那个字符。
子串统计
-
在 DAWG 上 DP。
-
或是在 parent tree 上直接统计。原因是一个结点的 \(\max len\) 已知,而它的 \(\min len\) 是其 \(fa\) 的 \(\max len + 1\),由此可以确定此结点上子串的长度和数量。如果结合 \(endpos\) 集合还可以得到位置。
endpos 相关
初始的元素(位置)在字符串每个前缀所在的结点上。
-
parent tree 是对 endpos 的“划分”,于是在 parent tree 上做线段树合并即可求出每个结点的 endpos,同时维护 endpos 的信息。如果需要在线,可以使用可持久化线段树合并。
-
有时只需要 endpos 中的一个点,或是其中的 \(\min,\max\),在 parent tree 上简单处理即可。
-
endpos 集合的大小表示结点中每个字符串出现的次数,同样容易在 parent tree 上 DP 出来。
另一种解释见“匹配”。
endpos 与 border,period
容易在一个结点的 endpos 集合中选两个位置,然后形成 border,period。
前缀 & 主链 相关
SAM 是在结尾加字符一点点构建的。
SAM 的 DAWG 的结构可以看做、画成以前缀结点形成的链为主干。
我们在做 SAM 结构相关统计时,可能可以区分前缀结点和其他结点(因其诞生方式称之为“分裂结点”)。见:P6152 [集训队作业2018] 后缀树节点数。
-
每个前缀结点上带着 endpos 的一个初始元素。可以认为 endpos 是前缀结点的信息合并。
-
每次在结尾添加一个字符(设其位置为 \(i\)),新增加的本质不同子串的起始位置为 \([\max len_p+1,\ldots,i]\)。此时的 \(p\) 是找到的第一个 \(ch_{p,c}\neq0\) 的 \(p\)。(?)
在 SAM 上定位一段前缀对应的结点是容易的(直接在 DAWG 上走),而定位一段区间时不妨从前缀对应结点(预处理出来)出发,倍增跳长辈。(前缀+倍增 定位)
所有前缀的所有后缀是所有子串。
扫描线时,扫 \(r\) 的同时维护前缀 \(S[1,r]\) 所在结点,添加的子串是此前缀的所有后缀,即此结点到根的路径。见 P6292 区间本质不同子串个数。
匹配(& ACAM)
SAM 有着类似 ACAM 的结构,可以用于字符串的匹配。
- parent tree 上连向 fa 的边类似于 ACAM 的 fail 边,让一段更短的后缀仍然匹配。
- DAWG 上的 ch[u][c] 边类似于 ACAM 的 ch[u][c] 边,在结尾增加一位匹配的字符。
于是我们可以对字符串 \(S\) 构建 SAM,用字符串 \(T\) 的每个前缀去匹配,得到字符串 \(T\) 的每个前缀的最大后缀匹配长度或对应的左端点。可以认为是双指针,两个指针分别是 \(T\) 的子串的左右端点。
于是可以求出两个串的 LCSubstring。进一步可以求多个串的。
类似 ACAM,为了在某些情况下保证复杂度可以把跳 fail 的过程压缩到 ch 里。(存疑)
另,可以认为 SAM 的 parent tree 上的一个“扩展后的状态”(比如匹配时,或者前缀,前缀匹配上前缀)是一条到根的链(和 ACAM 一样),这也从另一个角度解释了求 endpos 集合或相关信息的方法。
- 于是可以在所有前缀匹配完成后,将匹配信息通过简单“树形 DP”上传,实现对一条链的修改。应用见“最长公共子串”(还没写)。
可以认为匹配是一个双指针,我们主动移动右端点,考察左端点的变化,发现左端点单调不降(站在这一个右端点考虑上一个右端点即可说明)。于是可以在 DP 时尝试使用单调队列优化,例如 P4022 [CTSC2012] 熟悉的文章。
匹配时也许要注意应当从何处转移到下一个状态。也要注意匹配的复杂度。
\(T\) 在 \(S\) 子串中的匹配:P4770 [NOI2018] 你的名字,可持久化线段树合并维护 endpos,匹配时当前先不管区间限制走这步再跳 fa 到区间。
可能类似“你的名字”的匹配:HDU6583。(我还没 AC。)
这种匹配是说带其他限制的双指针,同样是先移动右端点,再跳 fa(移动左端点)直到合法。要求下一次匹配能继承这一次的匹配状态。
延迟匹配(将结尾可能用于匹配的字符存进队列):CF235C Cyclical Quest,submission。
关于压缩路径和不压缩路径的 DP 技巧,见这篇博客。(DP 时有一个不压缩路径的技巧。)
另有一种“一股脑”方法,见“广义 SAM”中的“匹配”。
匹配带有继承的性质,即继承上一位的匹配状态。思考时我们可以先考虑必要条件(忽略一些限制),考虑如何匹配,再加限制(可能要在原来匹配到的结点上跳 fa)。此时直接的做法是在原来的基础上倍增跳 fa。但可以考虑是否仍有继承性质,若仍有就可以省去倍增。
定位
- 字符串定位:在 DAWG 上走。
- \(S\) 子串(区间)定位(对 \(S\) 建 SAM):见“前缀相关”。
- \(T\) 子串(区间)定位(对 \(S\) 建 SAM):仍然考虑前缀,但可能前缀、查询的区间并不能匹配上。于是对每个前缀处理出它的最大后缀匹配长度或对应左端点(见“匹配”),再倍增跳长辈。
\(T\) 子串在 \(S\) 子串中(对 \(S\) 建 SAM):沿用 \(T\) 子串定位的方法,并考虑定位到的结点的 endpos 集合。
仅定位结点中的最长串/最短串(可以加上判断是否是最长串/最短串):字符串哈希。其实不一定是最长/最短串,只要串的个数可以接受就能哈希。字符串哈希应该要保证每个字符的编号 \(\in N_+\)(存疑,但总之不能有 \(0\),防前导零)。小心卡哈希。见:P6152 [集训队作业2018] 后缀树节点数。
后缀树相关
正串的 parent tree 是反串的后缀树。(?)
SAM 中两个结点的最长串的 LCSuffix 是它们在 parent tree 上 LCA 的 \(\max len\)。
但请注意,求一般的串的 LCSuffix 时,需要特判 \(\operatorname{LCA}(u,v)\in\{u,v\}\) 的情况。
通常我们遇到 LCP 且要使用 SAM 时,会将字符串翻转变为 LCS。
对哪个串建 SAM
并列(各个串要干的事相同):对于多个串的问题,可能要对最短串建 SAM,从而保证时间复杂度,需要小心。
不并列:我不知道。
对于区间(子串)的处理
对于在 SAM 上跑的串,我们通常先考虑前缀,然后考虑这个前缀的所有后缀(到根的路径)。
对于建 SAM 的串,我们一般在 endpos 集合中考虑,此时常结合线段树合并。
上述两种方案可能对同一个串同时使用。
“百鸟朝凤”
今天发现自己搞忘了,起个名字印象深刻点。
指多串在一个串的 SAM 上跑,并在结点或位置(endpos)上积累影响。
多串 LCSubstring 和 HDU4416 都是如此(存疑)。稍微深一点的理解可以看这篇:link,去里面搜“HDU 4416”。
注意 LCSubstring 的 mn 要先赋值成 len,相当于一个串自己和自己匹配。
基本应用
见巨佬 max 的课件。
感觉比较难理解的是多串 LCSubstring(在结点上累计,存疑),而比较难写的是可持久化线段树合并和区间定位。
支配对(离线支配对)
这个(支配对)其实是 DS 的内容,但在 SAM 上有常用的模型。
有时需要考虑 parent tree 上的 LCA,从而处理 LCSuffix。此时可以枚举 LCA,考虑 LCA 为它的点对,此时这些点对可能具有某种支配性质,我们取出未被支配的称为“支配对”。
支配对的数量往往远少于原先的点对数量,从而优化复杂度。通过树上合并找到支配对后,再用 扫描线+DS 来维护。
支配对一般是 endpos 集合中相邻的两个元素(位置)。此时采用树上合并算法(比如启发式合并 set),并寻找前驱后继即可求出支配对。
LCT
强制在线构建 SAM:使用 LCT 维护 parent tree,可以在强制在线构建 SAM 的过程中处理 parent tree 的树链、子树等信息。
在线支配对:相比离线寻找支配对,LCT 支持在线处理(但仍需要先建好 SAM,原因见“树上颜色段均摊”)。定位每个前缀,考虑前缀 \(i\) 与它对应结点 \(u\) 到根的路径上的每个点 \(x\),处理出 \(lst_x\) 表示 \(x\) 在前缀 \(i-1\) 中 endpos 中的 \(\max\)(即上一次能到 \(x\) 的前缀是哪个)。那么每对 \((lst_x,i)\) 就作为一对支配对。按照颜色段处理每一段的支配对的贡献。在处理完这一批支配对的贡献后,更新 \(lst_x\leftarrow i\)。
树上颜色段均摊:使用 LCT 维护,每次 Access 将到根的链染色,由 LCT 自身证明复杂度是 \(O(n\log n)\) 的。一条实链颜色相同。似乎不能使用 link、cut 操作,因为其中 Access 会破坏实链颜色相同的性质。
枚举 LCA 时的树链剖分
枚举一个点 \(x\) 到根的链上的点作为 LCA。重链剖分,画个图。
枚举到的 LCA 分为两类:
- \(x\) 在它的重子树里。一条重链上可能有若干个这种点,把这个询问挂在每条重链中这些点里的最深点上,这样的最深点对于 \(x\) 来说有 \(\log\) 个。对每条重链处理,自上而下,暴力合并(添加)轻子树结点和重链上的这个结点本身,之后处理此结点处的询问。这样就维护了重链的前缀信息,有一种在重链上扫描线的感觉。
- \(x\) 在它的轻子树里。这样的点对 \(x\) 来说只有 \(\log\) 个。考虑把 \(x\) 处的询问挂在这些点上,使用各种手段合并得到它的子树信息。(如果有“错解不优”就可以直接用子树信息了。)
相邻点 LCA 最深
- 和一个点 LCA 深度最深的点,是 dfs 序里它的前驱后继之一(抓出一个点集亦然)。
这个性质既可以用于求点集(比如按另一种标号的一段区间)中和 \(u\) LCA 最深的那个点 \(v\) 及它们的 LCA。
相邻点 LCA 与容斥
- (权值在点上)将点集 \(S\) 中的点按照 dfs 序排序,它们到根的路径并为(记 \(p(u)\) 为 \(u\) 到根的路径):
即做了一个容斥。
在涉及颜色(比如本质相同/同构),或者说需要去重时,可以使用这个容斥。见 P2336 [SCOI2012] 喵星球上的点名。
parent tree(后缀树性质)和 SA 的联系,LCA 和 RMQ 的关系
我们知道 parent tree 可以求(前缀的)LCS,又知道 SA 可以求(后缀的)LCP。(子串也都可以,只是更麻烦;可以先转成前后缀;parent tree 中子串似乎也可以直接做,加特判。)
我们知道 parent tree 是求 LCA,而 SA 是求区间 \(\min\)。这是否有某种联系?
再看后缀树,它也是求后缀的 LCP。是不是更直观了?
其中 \(x,y\) 是 dfs 序上的编号,\(x<y\)。
这就有点像 RMQLCA,有点像 LCA 和 RMQ 的互相转化,或许和 DFS 序求 LCA 有关。但这些东西我一样都还不会。/dk
DAWG 链剖分
想起自己其实写了一篇,但是没用过理解很浅,而且那个拆成 \(\log\) 条时间戳连续的链的作用我还不懂怎么用。
分隔符拼接(伪广义 SAM 之一)
有时还是有用的。
广义 SAM
其他部分和 SAM 类似,只是从单串扩展到多串。
参考 max 的课件和 这篇博客。
构建
三种构建方式:
- 每次 lst \(\leftarrow1\)。假。时间复杂度 \(O(\sum|S_i|)\)。
- 在 trie 上 dfs。假。时间复杂度 \(O(\sum|S_i|)\)。(存疑)
- 在 trie 上 bfs(按层次顺序构建)。真。时间复杂度 \(O(n)\)(Trie 结点数)。
这里复杂度没考虑字符集(复制 ch),其实可能要乘个 \(|\sum|\)。(存疑)
前两种也可以改成正确的,改良后的第一种方法支持在线构建。
空结点的问题。
标记 & 数颜色
我们可能需要标记一个点属于哪个(哪些)串。如何识别构成广义 SAM 的串之一的所有子串?
不妨识别它的每个前缀(匹配,走 ch 边),每个结点处匹配到的其实是到根的路径,即这个前缀的所有后缀。于是我们在前缀处标记再于 parent tree 上上传即可(可能需要树上合并)。
数颜色本质上是在去重。可以把构成广义 SAM 的每个串看成一种颜色。典型例题:P2336 [SCOI2012] 喵星球上的点名。
endpos
广义 SAM 的 endpos 集合由若干有序二元组组成,每个形如 \((i,j)\),其中 \(i\) 是串的编号,而 \(j\) 是 \(S_i\) 中的位置。
有时只会用二元组的一个维度。
我们仍然可以在 parent tree 上合并 endpos 集合,对于二元组的合并可以采用启发式合并。若把第一维作为下标,第二维作为值,也可以做线段树合并(线段树的“结构信息”是一维的),同样可以支持持久化。
一股脑
我们原本想拿去构成广义 SAM 的串,以及我们原本想拿去在广义 SAM 上跑的串,可以一起建广义 SAM。这样在跑的时候可能性质会好一些。(非常存疑)
对应地,在 AC 自动机上也可以这么搞。(存疑)
匹配
“一股脑”的方法在匹配上可能是有用的。我们把两类串都丢进广义 SAM 后,匹配时走的就是 Trie 的一条树链(非常存疑),也(存疑)是 ch 的一条链。
如果不“一股脑”丢进去,只把构成广义 SAM 的串丢进广义 SAM 的话,匹配时走的就是 ch 的一条链。
ACAM 同理(存疑)。
咕咕咕
基本子串结构
SAM 不对称,不够优美。尝试建立正串反串两个 SAM,再寻找其间的联系,从而更好地维护子串。因此基本子串结构是联系正反 SAM、维护子串的结构,且具有优美的对称性。
(不会对称压缩 SAM 呜呜呜。)
理解太浅了,而且还不会用,先咕了。
推荐学习:雨兔的博客、yyyyxh 的博客,前辈们真是太强啦!!!
某鸽子回来了
- 等价类的感性理解是“出现位置相同”。
- 二维平面,阶梯划分。行:正串 SAM 等价类(endpos 等价类),列:反串 SAM 等价类(beginpos 等价类)。
- 基本子串结构可以联系起一个子串在正串 SAM 和反串 SAM 中的等价类(结点)。
- 一个重要的事情是 SAM 的边在基本子串结构上对应的连边。
- 正串 SAM 的 parent tree 对应反串 SAM 的 DAWG;反串 SAM 的 parent tree 对应正串 SAM 的 DAWG。于是可以仅使用 parent tree 的结构来表示前后添加删除字符。(?)
- 可以利用阶梯划分和周长(行数列数之和)之和的性质来统计。
- 一个等价的结构是对称压缩 SAM。(?)
构建过程里的识别代表元稍微难以理解,解释先咕掉,有时间来写。可以看上面两位大佬的博客。
2025.4.19
2025.4.27
2025.4.29
2025.5.10
2025.5.13
2025.5.14
浙公网安备 33010602011771号