后缀结构2
1. 后缀平衡树
引入
后缀平衡树的每个节点维护一个后缀,平衡树中序遍历出来的就是后缀数组。
我们假设已经维护好了 \(S\) 的后缀平衡树,现在从前插入一个字符 \(c\),现在要把 \(c+S\) 插入。
假设我们现在比较 \(c+S\) 和 \(A\) 的大小,若首字母不同,那么已经比较出;
若首字母不同,就比较下一位,比较 \(S\) 和 \(A[2:]\),而 \(S\) 是已经被维护好的,在平衡树上找排名。
这样做单次操作是 \(O(\log^2 n)\),与二分哈希的复杂度相同。
发现我们花了 \(O(\log n)\) 在找排名上,如果我们能 \(O(1)\) 比较,单次操作会降低到 \(O(\log n)\)。
不妨给平衡树每个点赋一个权值,我们给每个点设一个区间 \([l,r]\),让这个点的权值赋为 \(mid\) 即可。
我们可以给根节点赋 \([1,10^{18}]\) 这种,保险使用实数,由于平衡树高为 \(O(\log n)\),所以精度是没问题。
应用
- P5212 SubString
注意到这是在后面插入字符的,所以我们不妨翻转序列,转化为在前面加字符,构建后缀平衡树。
现在我们查询 \(t\)(翻转后)出现次数,也就是查有多少个后缀满足 \(lcp(t,s[i:])=len(t)\).
假设平衡树维护的是数,现在查 \(x\) 出现次数,那么也就是 \(x\) 的排名与 \(x+1\) 的排名的差。
现在我们计算 \(t\) 的排名与 \(t'\) 的排名作差,其中 \(t'\) 是把 \(t\) 最后一位的字符 \(+1\)。
注意到因为 \(t\) 没在平衡树上,无法 \(O(\log)\) 操作。但是 \(\sum |t|\) 是有限的,所以我们可以暴力比较。
平衡树追求好写的话使用替罪羊树。
2. 后缀自动机
引入
SAM 是一个 DAG,其每条边代表一个字符,我们从 \(root\) 出发,沿着路径走可以走出所有子串。
SAM 的每个节点对应 endpos 相同的所有子串,长度是一个连续区间,诸如 \(\text{[ ]abc}\) 此类。
很显然的,两种子串的 endpos 要么是包含关系,要么是不相交。
SAM 中另一个重要的是后缀链接 link。若串 \(s\) 的最长后缀 \(t\) 满足 \(t\) 的 endpos 大小大于 \(s\) 的 endpos,那么 \(s\) 所对应的节点 \(u\) 把 \(link_u\) 设为 \(t\) 所对应的节点 \(v\)。
可见,后缀链接构成一棵树,而且,这与通过 endpos 集合的包含关系建出的树一致。
我们称这棵树是 parent tree。
构建
先设 \(len_u\) 表示节点 \(u\) 对应的串最长是多少。
构建 SAM 是一个在线的算法。我们从左到右加入字符串的每个字符,设当前加入字符 \(c\)。
我们设上一次插入完的状态是 \(las\),并创建一个状态 \(cur\),使 \(len_{cur}=len_{las}+1\)(整个串长)。
现在我们从节点 \(las\) 开始,往上跳后缀链接,若没有到 \(c\) 的转移,就加上 \(c\) 的转移到 \(cur\)。
如果我们找不到有 \(c\) 的转移的点,那么说明 \(c\) 是第一次出现,令 \(link_{cur}=root\),代表空串。
若我们找到了这样一个 \(p\),设转移到 \(q\),若 \(len_q=len_p+1\),那么 \(link_{cur}=q\)。
否则 \(len_q\) 一定大于 \(len_p+1\),说明 \(|endpos_p|>|endpos_q|\)。
诸如 \(\text{aabc aabc ab + c}\) 的: \(p\) 是 \(\text{ab}\),那么 \(q\) 是 \(\text{aabc}\),\(link_{cur}\) 不能为 \(q\),因为 \(cur\) 没有 \(\text{aabc}\) 的后缀。
那么我们复制 \(q\) 到 \(cln\),只令 \(len_{cln}=len_{p}+1\),其他不变,并且把 \(link_q\) 和 \(link_{cur}\) 设为 \(cln\)。
在上面那个例子里,现在 \(cln\) 即代表 \(\text{abc}\)。
最后,我们 \(p\) 继续跳后缀链接,如果有到 \(q\) 的转移,改为 \(cln\)。
因为 \(cln\) 和 \(q\) 所代表的 endpos 集合已经区别开了,\(cln\) 所代表的 endpos 多了新加入的位置。
应用
- 不同子串个数。
在 parent tree 上,每个点对应 \(\text{[ ]abc}\) 此类的串,而这些都是唯一的,个数是 \(len_u-len_{link_u}\)。 - 出现次数。
我们要求的也就是 endpos 集合的大小。维护 \(cnt_u\) 表示 \(|endpos_u|\)。
对于每个不是复制出来的节点,可见其一开始 \(cnt=1\)。
我们不妨在 parent tree 上 dp,因为 parent tree 就是 endpos 关系构成的树,
所以 \(cnt_u\) 加上其子树的 \(cnt_v\) 即可。 - 查询子串状态。
我们查询 \(s[l:r]\) 在 SAM 中的状态。
在构建 SAM 的时候我们已知 \(s[1:r]\) 的状态,那么我们在 parent tree 倍增找 \(len\ge r-l+1\) 的点。 - 所有出现位置。
即 endpos 集合里的所有数。
我们在插入 \(s[1:r]\) 时候一直 \(endpos_{cur}=\{len\}\),因为 endpos 集合树就是 parent tree,
所以我们做线段树合并,按题目需求决定是否可持久化。 - 最长公共子串。
我们现在假设已经构建了 \(S\) 的 SAM,现在考虑 \(T\)。
类比 AC 自动机,假设已经计算了前 \(i-1\) 位的答案,匹配到 \(u\),长度为 \(l\),现在插入 \(T_i\)。
我们不断跳 link,直到存在到 \(T_i\) 的转移,并转移过去就行了。
如果存在跳 Link,我们需要把 \(l\) 重设为 \(len(u)\)。
如果最后存在转移,令 \(l\gets l+1\)。 - 前缀最长公共后缀。
建出 SAM,设两个前缀对应 \(u,v\) 两个状态,求 Lca,那么 \(len(Lca)\) 为答案,因为 lca 为后缀。
那么后缀的 lcp 也就反建 SAM 即可。
用后缀树来理解,也就是后缀树的 lca,而后缀树就是反串 parent tree。 - 广义 SAM。
也就是在 trie 树上建立 SAM,按照 bfs 序建立。
例题
- P6640 [BJOI2020] 封印
考虑对 \(t\) 建出 SAM,然后对 \(s\) 上去跑一遍,计算出最大的 \(k_i\) 表示 \([i-k_i+1,i]\) 是合法的。
我们查询考虑二分答案 \(mid\),查询 \([l+mid-1,r]\) 中最大的 \(k\) 即可。 - P3346 [ZJOI2015] 诸神眷顾的幻想乡
因为叶子节点只有 \(20\) 个,所有叶子与叶子之间的路径的子路径覆盖所有路径。
考虑从叶子节点开始构成 \(20\) 棵字典树,并起来跑 GSAM 即可。 - P4022 [CTSC2012] 熟悉的文章
考虑二分答案后做一个 dp,dp 的转移范围用 SAM 求出,然后跑单调队列优化 dp。 - CF1037H Security
枚举第一位不同共 \(26|T|\) 种情况取在 SAM 中跑,需要判断 endpos 集合中是否存在 \([l,r]\) 里的元素。
我们不妨把 endpos 集合维护出来,因为 endpos 集合树满足父节点就是儿子的并,线段树合并。
一开始把所有插入的 \(cur\) 的 endpos 集合加入其插入的这个位置,复制的节点不用管。
然后通过 parent tree 合并上去,如果在线询问需可持久化新开节点即可。 - Loj #6198. 谢特
因为 lcp 是反串 SAM 后 parent tree 的 lca,我们不妨在树上做 01trie 合并。 - P4770 [NOI2018] 你的名字
类似 CF1037H Security,对 \(S\) 建 SAM 再线段树合并。
然后对于 \(T\),类似求最长公共子串,然后求出以每个位置为结尾的,左端点的合法范围。
然后 \(T\) 要求本质不同子串,所以 \(T\) 也要建 SAM,然后 fail 树上操作一下即可。

浙公网安备 33010602011771号