后缀结构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)\),所以精度是没问题。

应用

  1. 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 多了新加入的位置。

应用

  1. 不同子串个数。
    在 parent tree 上,每个点对应 \(\text{[ ]abc}\) 此类的串,而这些都是唯一的,个数是 \(len_u-len_{link_u}\)
  2. 出现次数。
    我们要求的也就是 endpos 集合的大小。维护 \(cnt_u\) 表示 \(|endpos_u|\)
    对于每个不是复制出来的节点,可见其一开始 \(cnt=1\)
    我们不妨在 parent tree 上 dp,因为 parent tree 就是 endpos 关系构成的树,
    所以 \(cnt_u\) 加上其子树的 \(cnt_v\) 即可。
  3. 查询子串状态。
    我们查询 \(s[l:r]\) 在 SAM 中的状态。
    在构建 SAM 的时候我们已知 \(s[1:r]\) 的状态,那么我们在 parent tree 倍增找 \(len\ge r-l+1\) 的点。
  4. 所有出现位置。
    即 endpos 集合里的所有数。
    我们在插入 \(s[1:r]\) 时候一直 \(endpos_{cur}=\{len\}\),因为 endpos 集合树就是 parent tree,
    所以我们做线段树合并,按题目需求决定是否可持久化。
  5. 最长公共子串。
    我们现在假设已经构建了 \(S\) 的 SAM,现在考虑 \(T\)
    类比 AC 自动机,假设已经计算了前 \(i-1\) 位的答案,匹配到 \(u\),长度为 \(l\),现在插入 \(T_i\)
    我们不断跳 link,直到存在到 \(T_i\) 的转移,并转移过去就行了。
    如果存在跳 Link,我们需要把 \(l\) 重设为 \(len(u)\)
    如果最后存在转移,令 \(l\gets l+1\)
  6. 前缀最长公共后缀。
    建出 SAM,设两个前缀对应 \(u,v\) 两个状态,求 Lca,那么 \(len(Lca)\) 为答案,因为 lca 为后缀。
    那么后缀的 lcp 也就反建 SAM 即可。
    用后缀树来理解,也就是后缀树的 lca,而后缀树就是反串 parent tree。
  7. 广义 SAM。
    也就是在 trie 树上建立 SAM,按照 bfs 序建立。

例题

  1. P6640 [BJOI2020] 封印
    考虑对 \(t\) 建出 SAM,然后对 \(s\) 上去跑一遍,计算出最大的 \(k_i\) 表示 \([i-k_i+1,i]\) 是合法的。
    我们查询考虑二分答案 \(mid\),查询 \([l+mid-1,r]\) 中最大的 \(k\) 即可。
  2. P3346 [ZJOI2015] 诸神眷顾的幻想乡
    因为叶子节点只有 \(20\) 个,所有叶子与叶子之间的路径的子路径覆盖所有路径。
    考虑从叶子节点开始构成 \(20\) 棵字典树,并起来跑 GSAM 即可。
  3. P4022 [CTSC2012] 熟悉的文章
    考虑二分答案后做一个 dp,dp 的转移范围用 SAM 求出,然后跑单调队列优化 dp。
  4. CF1037H Security
    枚举第一位不同共 \(26|T|\) 种情况取在 SAM 中跑,需要判断 endpos 集合中是否存在 \([l,r]\) 里的元素。
    我们不妨把 endpos 集合维护出来,因为 endpos 集合树满足父节点就是儿子的并,线段树合并。
    一开始把所有插入的 \(cur\) 的 endpos 集合加入其插入的这个位置,复制的节点不用管。
    然后通过 parent tree 合并上去,如果在线询问需可持久化新开节点即可。
  5. Loj #6198. 谢特
    因为 lcp 是反串 SAM 后 parent tree 的 lca,我们不妨在树上做 01trie 合并。
  6. P4770 [NOI2018] 你的名字
    类似 CF1037H Security,对 \(S\) 建 SAM 再线段树合并。
    然后对于 \(T\),类似求最长公共子串,然后求出以每个位置为结尾的,左端点的合法范围。
    然后 \(T\) 要求本质不同子串,所以 \(T\) 也要建 SAM,然后 fail 树上操作一下即可。
posted @ 2024-08-01 21:05  s1monG  阅读(20)  评论(0)    收藏  举报