推荐这篇Blog(写的比我详细多了Orz)
算法用途
在许多场合可以替代后缀树,也可以和其他算法结合搞许多事情,是一个很强大的东东。
各种东西
求后缀数组的整个过程其实就是基数排序,所以建议先学会基数排序。
① 后缀:即对于字符串
s
s
,即为s的一个后缀(
i∈[1,n]
i
∈
[
1
,
n
]
)。
②
rank
r
a
n
k
数组:
rank[i]
r
a
n
k
[
i
]
表示以
i
i
开头的后缀的排名(排名按字典序)。
③ 数组:即后缀数组,表示排名为
i
i
的后缀的开头在原字符串中的位置。
算法实现
暴力的求法就是把所有后缀存起来排一遍序,但是当太大时(比如1e6)空间就受不了,而且字符串是有长度的,因此时间复杂度为 O(n∗log2n2) O ( n ∗ l o g 2 n 2 ) ,也是不能够接受的。所以我们需要另寻出路。
不难发现
SA
S
A
数组与
rank
r
a
n
k
数组互为逆运算。
所以只要求出其中一个,另一个就出来了。
而求 rank r a n k 我们可以通过倍增实现。以目前的 rank r a n k 为第一关键字,以之前的 rank r a n k 为第二关键字进行基数排序。
时间复杂度 O(nlog2n) O ( n l o g 2 n ) (常数巨大)
给一张经典图自行体会(我好懒啊):
模板(代码解释)
以洛谷P3809为例:
#include<cstdio>
#include<cstring>
#include<algorithm>
#define MAXN 1000000
using namespace std;
int n,m;
int sa[MAXN+5],t[MAXN+5],rk[MAXN+5],rs[MAXN+5];
char s[MAXN+5];
void Rs(){//基数排序
memset(rs,0,sizeof(rs));//先把桶清零
for (int i=1;i<=n;i++) rs[rk[t[i]]]++;//放入桶里
for (int i=1;i<=m;i++) rs[i]+=rs[i-1];//累加得到排名(未离散)
for (int i=n;i>=1;i--) sa[rs[rk[t[i]]]--]=t[i];//更新至t数组中(t数组为临时数组)
}
void SA(){//构造SA数组
m=122; n=strlen(s+1);//题目所给的字符串中最大的字符ascll码为122('z')
for (int i=1;i<=n;i++){ rk[i]=s[i]; t[i]=i; }//初始化时rk直接取ascll码即可
Rs();
for (int k=1;k<=n;k*=2){
int p=0;
for (int i=n-k+1;i<=n;i++) t[++p]=i;//先把第二关键字为0的放进去
for (int i=1;i<=n;i++) if (sa[i]>k) t[++p]=sa[i]-k;//如果sa[i]>k那么sa[i]-k也可以作为第二关键字
Rs(); memcpy(t,rk,sizeof(t));//排序后copy一份
p=1; rk[sa[1]]=1;
for (int i=2;i<=n;i++)
if (t[sa[i]]==t[sa[i-1]]&&t[sa[i]+k]==t[sa[i-1]+k]) rk[sa[i]]=p;
//如果不并列新增一个排名
else rk[sa[i]]=++p;
if (p==n) break;//如果没有并列的表明已经排完
m=p;
}
}
int main(){
scanf("%s",s+1);
SA();
for (int i=1;i<=n;i++) printf("%d ",sa[i]);
return 0;
}
LCP相关
定义
LCP:最长公共前缀。
H[i]:sa[i]和sa[i-1]的LCP,即排名相近的两个后缀的最长公共前缀。
h[i]:H[rk[i]],代到前面就可以发现就是原串中两个相邻后缀的LCP。
那么sa[i]和sa[j]的前缀就是min(H[i+1]~H[j]),这个可以通过ST表预处理。
实现
对于h数组有这个性质:h[i]>=h[i-1]-1。这就意味着我们可以 O(n) O ( n ) 的求出h数组。
至于这个性质么。。。YY一下应该挺显然吧。 因为我太菜了不会证
而根据h[i]=H[rk[i]],就可以求出H数组了。
贴一下代码:
void mkh(){
for (int i=1,k=0;i<=n;i++){
if (k) k--;
while (s[i+k]==s[sa[rk[i]-1]+k]) k++;
H[rk[i]]=k;
}
}