📚【模板】后缀数组
后缀数组
对于一个字符串 \(pattern\),我们想求它的每一个后缀的排名 \(rank_i\),以及排名为 \(k\) 的后缀是哪一个 \(suffix_k\)。
这就是后缀数组的事了。
一般路过倍增方法( \(O(n\log^2 n)\) )
通过一张简明的图片,我们有了一种方法
不断地倍增 \(w\),每次以 \(rank_i\) 为第一关键字,\(rank_{i+w}\) 为第二关键字排序。
倍增需要 \(O(\log n)\) 次,每次排序需要 \(O(n \log n)\),时间复杂度 \(O(n \log^2 n)\)。
#include <stdio.h>
#include <string.h>
#include <bits/stl_algobase.h>
#include <bits/stl_algo.h>
const int N = 1048576;
char temp[N];
int rank[N<<1], sa[N];
int copy[N<<1];
int n, i;
void sa_get() {
n = strlen(temp+1);
for(int i = 1;i <= n;++i) {
sa[i] = i;
rank[i] = temp[i];
}
for(i = 1;i < n;i <<= 1) {
std :: sort(sa+1,sa+n+1,[](int _a,int _b) {
return rank[_a]^rank[_b] ?
rank[_a] < rank[_b] :
rank[_a+i] < rank[_b+i];
});
memcpy(copy,rank,(n+1)*sizeof(int));
for(int j = 0, k = 1;k <= n;++k) {
if(copy[sa[k]] == copy[sa[k-1]]&©[sa[k]+i] == copy[sa[k-1]+i])
rank[sa[k]] = j;
else
rank[sa[k]] = ++j;
}
}
}
signed main() {
scanf("%s",temp+1);
sa_get();
for(int i = 1;i <= n;++i)
printf("%d ",sa[i]);
}
基于计数排序的优化( \(O(n \log n)\) )
上面那份板子显然看起来不是很好
虽然过了板子题,但是单数据用时达到了令人谔谔的 \(600\,ms\),筛 \(5e8\) 以内的质数都比这快。
好了,咱们来点优化:
-
对于两个关键字使用基数排序和计数排序;
-
考虑到先把第二关键字顺序确定,再对第一关键字跑计数排序(计数排序是稳定排序);
-
每次更新一下值域 \(m\),不要每次对着 \(\operatorname{length}(pattern)\) 的值域跑计数排序;
-
减少内存不连续访问。
const int N = 1e6+10;
int rank[N], suffix[N];
int cnt[N], id[N], key_1[N], copy[N<<1];
int n, m = 127;
bool comp(int x,int y,int w) {
return copy[x] == copy[y]&©[x+w] == copy[y+w];
}
void get_sa(char *pattern) {
int p, i;
for(i = 1;i <= n;++i)
++cnt[rank[i] = pattern[i]];
for(i = 1;i <= m;++i)
cnt[i] += cnt[i-1];
for(i = n;i >= 1;--i)
suffix[cnt[rank[i]]--] = i;
for(int w = 1;;w <<= 1, m = p) {
for(p = 0, i = n;i > n-w;--i)
id[++p] = i;
for(i = 1;i <= n;++i)
if(suffix[i] > w)
id[++p] = suffix[i]-w;
//只要我们把最后的 w 个先放进数组,再按 suffix 数组的顺序放
//那么第二关键字就是有序的了,而计数排序是稳定排序,不会破坏第二关键字的顺序
memset(cnt,0,(m+1)*sizeof(int));
for(i = 1;i <= n;++i)
++cnt[key_1[i] = rank[id[i]]];
for(i = 1;i <= m;++i)
cnt[i] += cnt[i-1];
for(i = n;i >= 1;--i)
suffix[cnt[key_1[i]]--] = id[i];
//计数排序过程
memcpy(copy+1,rank+1,n*sizeof(int));
for(p = 0, i = 1;i <= n;++i)
rank[suffix[i]] = comp(suffix[i],suffix[i-1],w) ? p : ++p;
//计算新的 rank 数组
if(p == n) {
for(i = 1;i <= n;++i)
suffix[rank[i]] = i;
break;
}
}
}
这样就可以快乐地切掉板子题了!
\(\textrm{luogu P3809 【模板】后缀排序}\)
现在时间开销就少多了:
后缀数组的一些应用
height 数组
\(\operatorname{lcp}(S,T)\):表示字符串 \(S,T\) 的最长公共前缀。
下文记 \(\operatorname{lcp}(i,j)\) 为字符串 \(pattern\) 的后缀 \(i\) 与后缀 \(j\) 的最长公共前缀。
那么 \(height_i\) 定义为字符串 \(pattern\) 第 \(i\) 名后缀与其前一名后缀的最长公共前缀,记为 \(height_i = \operatorname{lcp}(suffix_i,suffix_{i-1})\)。
利用 \(rank\) 数组方便地求 \(height\) 数组( \(O(n)\) )
有一个引理:
证明
int height[N];
int get_height(char *pattern) {
for(int i = 1, k = 0;i <= n;++i) {
if(rank[i] == 0)
continue;
if(k)
--k;
while(pattern[i+k] == pattern[suffix[rank[i]-1]+k])
++k;
height[rank[i]] = k;
}
}