字符串进阶算法 1 :exKMP、SA、SAM、后缀平衡树

exKMP / Z函数

对于字符串 ss每个后缀,求其与 ss最长公共前缀 ziz_i

要求 O(n)O(n)

我们考虑当前在对 ii 进行操作。

之前已经有一段区间 [l,r][l,r] 满足 s[1,rl+1]=s[l,r]s[1,r-l+1]=s[l, r]rr 最大。

  1. iri\le r,则直接继承 zi=zil+1z_i=z_{i - l + 1}

    image-20260709100309834

  2. 接下来暴力匹配,只要匹配成功,此时 rr 必然变大

SA

对字符串 ss所有后缀按字典序排序

要求 O(nlogn)O(n\log n)

我们考虑先比较所有后缀的第一位,这可以使用桶排 O(n)O(n)

接下来我们再通过倍增比较前2位、前4位、8位、16位、32位:

image-20260709100758494

SAM

O(n)O(n) 个节点存 ss 的所有后缀

对于每个节点,它代表的串是长这样子的:

image-20260709101333545

我们有两棵树:

  • fail树:代表去掉红色段后,蓝色段所代表的节点(fail[i]:i的父亲)
  • nxt树:代表在末尾加入一个字符 cc 后,会去到哪个节点

在线构建。我们考虑加入一个字符 cc新建一个节点 uu

  1. 连nxt,前面所有段的 nxt[x][c]=u (在 nxt[x][c] 为空时)

    寻找前面这些段的过程只需要不断跳fail即可。(即不断右移红色段)

  2. 如果现在出现一个 nxt[p][c] = q = != 0,我们就陷入讨论了

    • 如果 len[q]=len[p]+1len[q]=len[p]+1,即 qq 对于 pp 来说,只在后面添加字母,没有延长前面的红色段,那么就可以直接 failu=qfail_u=q

    • 否则,我们需要把 qq 前面的红色段切成两段。不妨记长的一段是 qq,短的一段是 cqcq。那样子和 p,up,u 产生关联的都是 cqcq 了(u的fail和p的next)

      qqcqcq 的关系则为 failq=cqfail_q=cq

后缀平衡树

对于SA解决的问题,我们现在要带修(头加头减)

我们考虑建一棵平衡树,维护每个后缀,考虑我们从后往前,从短往长,插入每个后缀,考虑我们在每个节点上与那个节点的字符串 AA 比较:

  1. 直接逐位比较法:复杂度很大

  2. 如果我们当前加入的是 s[i:n]s[i:n],而我们已经知道了 s[i+1:n]s[i+1:n] 的排位了。

    所以我们可以先比较第一位 s[i]s[i],如果相同,我们就用后缀平衡树查询 s[i+1:n]s[i+1:n] 的排位与 nn 比较

    复杂度 O(nlog2n)O(n\log ^2n)

  3. 我们如果让每个节点对应一个实数区间的话,我们就可以直接比较这个实数区间的大小关系

    复杂度 O(nlogn)O(n\log n)

    但这样子可能需要一些不旋转的平衡树,比如替罪羊等。

如果题目全程是尾插尾删的话,我们可以倒转过来变成头插头删。

询问某个串 ttss 中的出现次数

相当于是多少个后缀的前缀为 tt

我们直接在 tt 后面加入一个极小字符和一个极大字符,然后查询这两个串的排位差即可。