后缀数组(SA)学习笔记
后缀数组
以下,我们将字符串的元素从 \(1\) 开始标号。后缀 \(i\) 表示以 \(i\) 开头的后缀。
定义
记 \(sa_i\) 表示将所有后缀按字典序排序后,第 \(i\) 小后缀的标号。
记 \(rk_i\) 表示后缀 \(i\) 的排名。
后缀排序
两只 log 的做法
我们采用倍增法,枚举二的幂次 \(w\)。如果对每个 \(i\) 求出 \(s[i\dots i+w-1]\) 的排名 \(rk_i\),那么发现 \(s[i\dots i+2w-1]\) 的排名就是以 \(rk_i\) 为第一关键字,\(rk_{i+w}\) 为第二关键字排序后的排名。
那么我们每新的一轮用上一轮更新好的 \(rk\) 给 \(sa\) 排序,然后再根据 \(sa\) 给 \(rk\) 重新标号。
如果对 \(sa\) 的排序采用 std::sort 就可以做到 \(O(n\log ^2n)\),实现如下。
这里有一个小优化,当一轮已经出现 \(n\) 个排名时就可以不用做了(即 if(p==n) break;) 。
#include<bits/stdc++.h>
using namespace std;
int n;string s;
const int N=1e6+5;
int sa[N],rk[N<<1],lrk[N<<1];
int w;
int main(){
// freopen("in.in","r",stdin);
// freopen("out.out","w",stdout);
std::ios::sync_with_stdio(0);
s=" ";string tmp;
cin>>tmp;n=tmp.size();s+=tmp;
for(int i=1;i<=n;i++){
sa[i]=i,rk[i]=s[i];
}
for(w=1;w<n;w<<=1){
sort(sa+1,sa+1+n,
[](const int &x,const int &y)->bool{
if(rk[x]==rk[y])return rk[x+w]<rk[y+w];
return rk[x]<rk[y];
});
memcpy(lrk,rk,sizeof rk);
int p=0;
for(int i=1;i<=n;i++){
if(i>1&&lrk[sa[i-1]]==lrk[sa[i]]&&lrk[sa[i-1]+w]==lrk[sa[i]+w]){
rk[sa[i]]=p;
}
else rk[sa[i]]=++p;
}
if(p==n)break;
}
for(int i=1;i<=n;i++)cout<<sa[i]<<" ";
}
一只 log 的做法
由于 \(sa\) 排序时比较的是排名 \(rk\),而排名的值域是 \(O(n)\) 的,于是使用基数排序就可以做到 \(O(n\log n)\)。
其中 \(p\) 代表当前排名的值域,显然第一次的值域为字符集大小。
const int N=1e6+5;
char a[N];
int n,sa[N],lsa[N],rk[N*2],lrk[N*2];
int sm[N];
signed main(){
read(a+1), n=strlen(a+1);
fo(i,1,n) sa[i]=i,rk[i]=a[i];
if(n==1) rk[1]=1;
for(int l=1,t=0,p=128;l<n;l<<=1,p=t,t=0) {
memcpy(lsa,sa,sizeof sa);
fo(i,0,p) sm[i]=0;
fo(i,1,n) sm[rk[sa[i]+l]]++;
fo(i,1,p) sm[i]+=sm[i-1];
fd(i,n,1) sa[sm[rk[lsa[i]+l]]--]=lsa[i];
memcpy(lsa,sa,sizeof sa);
fo(i,0,p) sm[i]=0;
fo(i,1,n) sm[rk[sa[i]]]++;
fo(i,1,p) sm[i]+=sm[i-1];
fd(i,n,1) sa[sm[rk[lsa[i]]]--]=lsa[i];
memcpy(lrk,rk,sizeof rk);
fo(i,1,n)
if(lrk[sa[i-1]]==lrk[sa[i]]&&lrk[sa[i-1]+l]==lrk[sa[i]+l])
rk[sa[i]]=t;
else
rk[sa[i]]=++t;
if(t==n) break;
}
fo(i,1,n) write(sa[i],' ');
return 0;
}
height 数组
LCP
记 \(lcp(i,j)\) 为后缀 \(i\) 和后缀 \(j\) 的最长公共前缀长度。
height 数组
定义数组 \(hei_i=lcp(sa_i,sa_{i-1})\),\(hei_1=0\)。
O(n) 求 height 数组
引理:\(hei_{rk_i}\ge hei_{rk_{i-1}}-1\)。
暴力实现即可。因为 \(hei\) 不超过 \(n\),最多减 \(n\) 次,所以最多加 \(2n\) 次,复杂度 \(O(n)\)。
for(int i=1,k=0;i<=n;++i) {
if(rk[i]==1) {k=0; continue;}
if(k) --k;
while(s[i+k]==s[sa[rk[i]-1]+k]) ++k;
hei[rk[i]]=k;
}
求两子串的 LCP
根据 LCP 的传递性,对于 \(i<j\),有 \(lcp(sa_i,sa_j)=\min(hei_{i+1},\dots,hei_{j})\)。
于是转化为了 RMQ 问题。
本质不同子串数
子串就是后缀的前缀,计算每个后缀有多少个前缀,再减掉重复的即可。
按 \(sa\) 枚举后缀,那么每个后缀新增的前缀就是其所有前缀减去与上一个后缀的 LCP。所以本质不同子串数等于下列式子:
检查原串中一个子串的出现次数
求出 \(hei\) 数组后,如果要查询子串 \([l,r]\) 的出现次数,那么从 \(rk_l\) 开始在 \(hei\) 上往左右二分,使得 \(lcp\ge r-l+1\),那么分别二分出左右端点 \(L,R\),此时出现次数即 \(R-L+1\)。查询 \(hei\) 的最值使用 ST 表,则单次查询是 \(O(\log n)\)。

浙公网安备 33010602011771号