后缀数组
SA 数组
现在给你一个字符串,要在 \(\mathcal O(n\log n)\) 内解决如下问题:把这个字符串的所有非空后缀按字典序从小到大排序,然后按顺序输出后缀的第一个字符在原串中的位置。
暴力排是 \(\mathcal O(n^2\log n)\) 的,这里可以二分+哈希,即比较后缀时二分出 LCP 然后比较下一个字符,但也只能做到 \(\mathcal O(n\log^2n)\),需要更优做法。
后缀数组基于倍增和基数排序,可以将时间复杂度做到 \(\mathcal O(n\log n)\)。
比较字符串肯定从第一位开始比,那就将字符串的所有字符排序,第一个字符不同的字符串就比好了,但第一个字符相同的,需要继续比较第二位、第三位等,复杂度就起飞了。
考虑如何优化第二位的比较,发现第 \(i\) 个后缀的第二个字符就是第 \(i+1\) 个后缀的第一个字符,因此将第一个字符作为第一关键字,将第二个字符作为第二关键字即可。
类似地,使用倍增,现在已经得到了长度为 \(w\) 的结果,对于 \([i,i+2w-1]\),可以分成两段 \([i,i+w-1]\) 和 \([i+w,i+2w-1]\),直接把它们的排名拿过来做双关键字排序即可。直到每个后缀的名次都不同就可以结束算法了。
考虑代码怎么写。
设 \(\text{sa}[i]\) 表示当前倍增长度时排名为 \(i\) 个后缀的起始位置,\(\text{rk}[i]\) 表示起始位置为 \(i\) 的后缀在当前轮的排名。倍增的时候,假设当前长度为 \(2w\),我们手上就有上一轮的 \(\text{sa}\),也就是长度为 \(w\) 的后缀数组。由于要做的是双关键字排序,因此再设 \(\text{tp}[i]\) 表示第二关键字排名为 \(i\) 个后缀的起始位置,处理 \(\text{tp}\) 的代码如下:
for (int i = n - w + 1; i <= n; i++) tp[++p] = i;
for (int i = 1; i <= n; i++) if (sa[i] > w) tp[++p] = sa[i] - w;
第一行的意思是,第二关键字为空的后缀肯定排在前面;第二行按照字典序大小处理有第二关键字的后缀,设其为起始位置为 \(i\),那么它在本轮中对应的就是 \(i-w\) 这个后缀。
这时再做一个神秘的 sort 就能得到新一轮的 \(\text{sa}\),这个稍后再说。
\(\text{rk}\) 可能会有重复,记录一下到底出现了几种排名。然后就做双关键字比较:
swap(rk, tp), p = rk[sa[1]] = 1;
for (int i = 2; i <= n; i++) rk[sa[i]] = (tp[sa[i]] == tp[sa[i - 1]] && tp[sa[i] + w] == tp[sa[i - 1] + w]) ? p : ++p;
PS:这时候之前的 \(\text{tp}\) 和 \(p\) 没有用了,重新利用一下。
如果没有重复排名就退出:
if (p == n) return;
再来看这个神秘的 sort:
void sort() {
for (int i = 1; i <= m; i++) a[i] = 0;
for (int i = 1; i <= n; i++) a[rk[i]]++;
for (int i = 1; i <= m; i++) a[i] += a[i - 1];
for (int i = n; i >= 1; i--) sa[a[rk[tp[i]]]--] = tp[i];
}
其中 \(m\) 是当前字符集大小(有多少种 \(\text{rk}\)),前三步是在处理桶,第四步这是在?
考虑 \(w\rightarrow2w\) 时的变化,第一关键字的相对顺序不会改变,会变化的只有 \(\text{rk}\) 相同的后缀,倒序枚举,那么 \(a[\text{rk}[\text{tp}[i]]]\) 就是第一关键字相同的后缀中,第二关键字最大的后缀的排名,可以用来更新 \(\text{sa}\) 数组。
完整代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 1000005;
int n, m = 62, rk[N], tp[N], sa[N], a[N];
char s[N];
void init() {
for (int i = 1; i <= n; i++) {
if (s[i] <= '9') rk[i] = s[i] - '0' + 1;
else if (s[i] <= 'Z') rk[i] = s[i] - 'A' + 11;
else rk[i] = s[i] - 'a' + 37;
tp[i] = i;
}
}
void sort() {
for (int i = 1; i <= m; i++) a[i] = 0;
for (int i = 1; i <= n; i++) a[rk[i]]++;
for (int i = 1; i <= m; i++) a[i] += a[i - 1];
for (int i = n; i >= 1; i--) sa[a[rk[tp[i]]]--] = tp[i];
}
void sufs() {
for (int w = 1, p = 0; w <= n; m = p, p = 0, w <<= 1) {
for (int i = n - w + 1; i <= n; i++) tp[++p] = i;
for (int i = 1; i <= n; i++) if (sa[i] > w) tp[++p] = sa[i] - w;
sort(), swap(rk, tp), p = rk[sa[1]] = 1;
for (int i = 2; i <= n; i++) rk[sa[i]] = (tp[sa[i]] == tp[sa[i - 1]] && tp[sa[i] + w] == tp[sa[i - 1] + w]) ? p : ++p;
if (p == n) return;
}
}
int main() {
scanf("%s", s + 1), n = strlen(s + 1);
init(), sort(), sufs();
for (int i = 1; i <= n; i++) printf("%d ", sa[i]);
}
Height 数组
后缀数组很有用的一点是可以求出 \(\text{height}\) 数组,定义为:
即第 \(i\) 名和第 \(i-1\) 名后缀的最长公共前缀。
为了求出 \(\text{height}\) 数组,需要一个引理:
证明:当 \(\text{height}[\text{rk}[i-1]]\le1\) 时显然成立,考虑 \(\text{height}[\text{rk}[i-1]]>1\) 的情况。
根据定义,有 \(\text{lcp}(\text{sa}[\text{rk}[i-1]],\text{sa}[\text{rk}[i-1]-1])>1\),意味着后缀 \(i-1\) 和 \(\text{sa}[\text{rk}[i-1]-1]\) 有长度为 \(\text{height}[\text{rk}[i-1]]\) 的 LCP,设其为 \(cS\),其中 \(c\) 是一个字符,\(S\) 是一个长度为 \(\text{height}[\text{rk}[i-1]]-1\) 的字符串,由于 \(\text{height}[\text{rk}[i-1]]>1\),其长度 \(>0\),即非空。
那么后缀 \(i-1\) 可以表示成 \(cST\),后缀 \(\text{sa}[\text{rk}[i-1]-1]\) 可以表示成 \(cSU\),后缀 \(i\) 可以表示成 \(ST\),后缀 \(\text{sa}[\text{rk}[i-1]-1]+1\) 可以表示成 \(SU\)。由后缀数组的定义,可知 \(U<T\),\(U\) 可能为空串,但 \(T\) 非空。
由于后缀 \(\text{sa}[\text{rk}[i]-1]\) 仅比后缀 \(i\) 小一位,而 \(SU<ST\),则:
\[SU\le\text{suffix}[\text{sa}[\text{rk}[i]-1]]<ST \]那么后缀 \(\text{sa}[\text{rk}[i]-1]\) 与后缀 \(i\) 至少有 LCP 为 \(S\),也即 \(\text{height}[\text{rk}[i]]\ge\text{height}[\text{rk}[i-1]]-1\)。
根据这个引理直接做即可,复杂度是 \(\mathcal O(n)\) 的。
void geth() {
for (int i = 1, j = 0; i <= n; i++) {
j -= !!j;
while (s[i + j] == s[sa[rk[i] - 1] + j]) j++;
h[rk[i]] = j;
}
}
\(\text{height}\) 数组有什么用呢,例如查询两个后缀 \(x,y\) 的 LCP,有:
感性理解一下,\(\text{height}\) 不变说明前面那些位一直没有变过,如果 \(\text{height}\) 减小了,由于 \(\text{sa}\) 是排好序的,不可能再变回来,因此取 \(\min\) 就行。这是个 RMQ 问题,可以用 ST 表做。
再比如说求本质不同子串数量,由于子串 = 后缀的前缀,只需要考虑加入一个后缀时的贡献即可。按照 SA 的顺序枚举,新加入的就是除去和上一个后缀的 LCP 的前缀,那么答案为:
还有更多的用处,到时候做到了再说吧。
浙公网安备 33010602011771号