后缀数组(SA)学习笔记
后缀数组
问题引入
给你一个长度为 \(n\) 的字符串 \(s\),将它的所有后缀按字典序排序。
解法
后缀数组主要关系到两个数组:\(sa\) 和 \(rk\)。
其中,\(sa[i]\) 表示将所有后缀排序后第 \(i\) 小的后缀的编号,也是所说的后缀数组,也称编号数组;
\(rk[i]\) 表示后缀 \(i\) 的排名,也称排名数组。
这两个数组满足性质:\(sa[rk[i]]=rk[sa[i]]=i\)。
\(O(n^2\log n)\) 做法
直接存储每一个后缀 sort 即可。
\(O(n\log^2n)\) 做法
考虑倍增,每次按照相邻 \(2\) 个长度为 \(w=2^k\) 的子串排名作为排序的第一第二关键字对编号数组排序,越界部分排名视为 \(0\)。于是 \(\lfloor\log_2(n-1)\rfloor+1\) 次排序,单次复杂度 \(O(n\log n)\),总时间复杂度 \(O(n\log^2n)\)。
\(O(n\log n)\) 做法
使用计数排序优化即可。具体实现细节见代码。
char s[N];
int n,m,p,rk[N],oldrk[N*2],sa[N],id[N],cnt[N];
void main(){
scanf("%s",s+1);
n=strlen(s+1);
m=128; // 初始字符集
for(int i=1;i<=n;i++)cnt[rk[i]=s[i]]++; // 单个字符计数排序
for(int i=1;i<=m;i++)cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--)sa[cnt[rk[i]]--]=i; // 倒序循环,第一关键字相同时索引大的排在后面
for(int w=1;;w<<=1,m=p){
int cur=0;
for(int i=n-w+1;i<=n;i++)id[++cur]=i; // 第二关键字无需计数排序
for(int i=1;i<=n;i++) // 其实就是把超出字符串范围的 sa[i] 放到 sa 数组头部
if(sa[i]>w)id[++cur]=sa[i]-w; // 然后把剩下的依原顺序放入
for(int i=0;i<=m;i++)cnt[i]=0;
for(int i=1;i<=n;i++)cnt[rk[i]]++;
for(int i=1;i<=m;i++)cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--)sa[cnt[rk[id[i]]]--]=id[i]; // 倒序循环,第一关键字相同时在 id 中靠后的元素排在后面
p=0;
for(int i=1;i<=n;i++)oldrk[i]=rk[i];
for(int i=1;i<=n;i++)
if(oldrk[sa[i]]==oldrk[sa[i-1]]&&oldrk[sa[i]+w]==oldrk[sa[i-1]+w])
rk[sa[i]]=p;
else
rk[sa[i]]=++p;
if(p==n)break;
}
for(int i=1;i<=n;i++)writeu(sa[i]),SPA;
}
应用
寻找最小的循环移动位置
复制一遍后跑 SA 即可。例:P4051 [JSOI2007] 字符加密
在字符串中找子串
在线地在字符串 \(S\) 中寻找模式串 \(T\)。子串必然是后缀的前缀,只需要预先跑出 SA 二分就可以做到 \(O(|T|\log|S|)\) 的复杂度。
height 数组
定义:\(lcp(i,j)\) 表示后缀 \(i\) 与后缀 \(j\) 的最长公共前缀长度。
\(height[i]=lcp(sa[i],sa[i-1])\),即第 \(i\) 名的后缀与它前一名的后缀的最长公共前缀。\(height[1]\) 可以视作 \(0\)。
解法
我们有 \(height[rk[i]]\ge height[rk[i-1]]-1\),因此暴力求即可,复杂度是 \(O(n)\) 的。
for(int i=1,k=0;i<=n;i++){
if(rk[i]==1)continue;
if(k)--k;
while(s[i+k]==s[sa[rk[i]-1]+k])++k;
height[rk[i]]=k;
}
应用
两子串最长公共前缀
我们有 \(lcp(sa[i],sa[j])=\min\{height[i+1..j]\}\),RMQ 即可。
比较一个字符串的两个子串的大小关系
假设需要比较的是 \(A=S[a..b]\) 和 \(B=S[c..d]\) 的大小关系。
若 \(lcp(a,c)\ge\min(|A|,|B|)\),\(A<B\iff|A|<|B|\)。
否则,\(A<B\iff rk[a]<rk[c]\)。
不同子串的数目
即全部子串减去重复统计的“后缀的前缀”,答案即为 \(\frac{n(n+1)}2-\sum_{i=2}^nheight[i]\)。

浙公网安备 33010602011771号