后缀数组学习笔记
什么是后缀数组
后缀的数组
后缀数组是指将一个字符串的所有后缀按照字典序从小到大排序后的数组。
这里我们用 i i i代表从 i i i开始直到结尾的后缀。
定义两个数组 s a , r k sa, rk sa,rk
s a i sa_i sai表示排名为 i i i的后缀的编号
r k i rk_i rki表示编号为 i i i的后缀的排名
容易发现 r k s a i = s a r k i = i rk_{sa_i} = sa_{rk_i} = i rksai=sarki=i
例:
s = “ a b a b a ” s = “ababa” s=“ababa”
i i i | 编号为 i i i的后缀 | r k i rk_i rki | s a i sa_i sai |
---|---|---|---|
1 | a b a b a ababa ababa | 3 | 5 |
2 | b a b a baba baba | 5 | 3 |
3 | a b a aba aba | 2 | 1 |
4 | b a ba ba | 4 | 4 |
5 | a a a | 1 | 2 |
(注:我们认为空串字典序是最小的,即一个串的任意前缀的字典序都小于这个串)
怎么求后缀数组
一个很显然的方法是把它们都拿出来 s o r t sort sort一下
这个方法显然是不对的,因为 s t r i n g c o m p a r e string compare stringcompare的复杂度是 O ( ∣ S ∣ ) O(|S|) O(∣S∣)的,这么做 O ( ∣ s ∣ 2 l o g ∣ s ∣ ) O(|s| ^ 2 log |s|) O(∣s∣2log∣s∣)你人就没了
这里介绍比较普遍的做法:倍增法
大体思想:
第 i i i次将从每个元素开始,长度为 2 i 2 ^ i 2i的所有子串排序。
这样做 l o g ∣ s ∣ log|s| log∣s∣次之后,当前排好序的所有串就都为原串后缀了。
具体实现:
假设已经把所有长度为 2 i 2 ^ i 2i的字符串排好了序。
要想对长度为 2 i + 1 2 ^ {i + 1} 2i+1的字符串排序,显然我们需要先比较前 2 i 2 ^ i 2i位,如果不相同则比较后 2 i 2 ^ i 2i位。
这时我们发现,前后 2 i 2 ^ i 2i位都不需要逐位比较了,因为上一轮已经比较完了,所以只需要比较上一轮排出来的 r k rk rk即可
一个简单的做法是把所有点按照 p a i r ( r k j , r k j + 2 i ) s o r t pair(rk_j, rk_{j + 2 ^ i}) sort pair(rkj,rkj+2i)sort一遍。这样显然是 O ( ∣ s ∣ l o g 2 ∣ s ∣ ) O(|s| log^2 |s|) O(∣s∣log2∣s∣)网上博客貌似也有这样写的,大多数题目或许也能过,但是可以通过一种并不是很麻烦的做法把复杂度降低到 O ( ∣ s ∣ l o g ∣ s ∣ ) O(|s| log |s|) O(∣s∣log∣s∣)
基数排序
我们注意到, 1 < = r k i < = ∣ s ∣ 1 <= rk_i <= |s| 1<=rki<=∣s∣
值域较小的情况下,我们有一种不基于比较的线性排序法——基数排序法
其实非常简单,就是将每个值的数量算出来,再在值上从前到尾做一遍前缀和,这样一个值上记录的就是小于等于这个值的数字个数了。到这里已经很显然了,因为一个数排名其实就是小于这个数的数字个数+1 。注意一下相等的情况即可。
代码:
void sort(int a[])
{
int m = 0;
for(int i = 1; i <= n; i++)
{
cnt[a[i]]++;
m = max(m, a[i]);
}
for(int i = 1; i <= m; i++)
cnt[i] += cnt[i - 1];
for(int i = 1; i <= n; i++)
s[cnt[a[i]]--] = a[i];
for(int i = 1; i <= n; i++)
a[i] = s[i];
for(int i = 1; i <= m; i++)
cnt[i] = 0;
现在我们要进行双关键字的排序,怎么办呢?如果要将第一关键字乘上一个足够大的值再加上第二关键字,会导致值域退化到平方级别。但实际上,由于基数排序是稳定排序算法,我们只需先对第二关键字进行排序,再对第一关键字进行排序即可。在计算 s s s数组的时候倒序计算,就能确保第一关键字相等的元素能够按照原序也就是第二关键字排序。证明较为简单,请读者自行思考。
到这里,我们就已经完全掌握了后缀数组的构造。在循环最后还需更新 r k rk rk数组,注意相同元素排名需一致即可。
模板:后缀排序
代码:
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int N = 2e6 + 5;
struct SA
{
int n, rk[2 * N], sa[N], trk[N], tmp[N], cnt[2 * N];
char s[N];
void read()
{
scanf("%s", s + 1);
n = strlen(s + 1);
}
void build()
{
int m = 0;
for(int i = 1; i <= n; i++)
{
cnt[s[i]]++;
m = max(m, int(s[i]));
}
for(int i = 1; i <= m; i++)
cnt[i] += cnt[i - 1];
for(int i = n; i >= 1; i--)
sa[cnt[s[i]]--] = i;
rk[sa[1]] = 1;
int p = 1;
for(int i = 2; i <= n; i++)
rk[sa[i]] = ((s[sa[i]] == s[sa[i - 1]]) ? p : ++p);
for(int k = 1; k <= n; k <<= 1)
{
for(int i = 0; i <= m; i++)
cnt[i] = 0;
m = 0;
for(int i = 1; i <= n; i++)
{
cnt[rk[i + k]]++;
m = max(m, rk[i + k]);
}
for(int i = 1; i <= m; i++)
cnt[i] += cnt[i - 1];
for(int i = n; i >= 1; i--)
tmp[cnt[rk[i + k]]--] = i;
for(int i = 0; i <= m; i++)
cnt[i] = 0;
m = 0;
for(int i = 1; i <= n; i++)
{
cnt[rk[tmp[i]]]++;
m = max(m, rk[tmp[i]]);
}
for(int i = 1; i <= m; i++)
cnt[i] += cnt[i - 1];
for(int i = n; i >= 1; i--)
sa[cnt[rk[tmp[i]]]--] = tmp[i];
trk[sa[1]] = 1;
int p = 1;
for(int i = 2; i <= n; i++)
trk[sa[i]] = ((rk[sa[i]] == rk[sa[i - 1]] && rk[sa[i] + k] == rk[sa[i - 1] + k]) ? p : ++p);
for(int i = 1; i <= n; i++)
rk[i] = trk[i];
}
}
}S;
int main()
{
S.read();
S.build();
for(int i = 1; i <= S.n; i++)
printf("%d ", S.sa[i]);
return 0;
}
height数组
如果只能做模板题,后缀数组貌似也没有什么用。真正让后缀数组发挥出威力的是 h e i g h t height height数组。
h e i g h t height height数组的定义: h e i g h t i height_i heighti表示排序后排名为 i i i的的后缀和排名为 i − 1 i - 1 i−1的后缀的最长公共前缀的长度
即 h e i g h t i = s t r l e n ( L C P ( s a i , s a i − 1 ) ) height_i = strlen(LCP(sa_i, sa_{i - 1})) h