后缀数组(SA)
后缀数组
· 前言
· 定义
后缀数组分为两个数组:
\(sa_i\) 表示第 \(i\) 大的后缀的第一个字符在原串中的位置。
\(rk_i\) 表示第一个字符在原串中的位置为 \(i\) 的后缀的排名。(两个相同的后缀排名相同)
易知:
\(rk_{sa_i}\) 表示第 \(i\) 名。
\(sa_{rk_i}\) 表示 \(i\) 这个位置。
· 算法流程
\(O(nlog^2n)\)做法
我们考虑倍增,已知长度为 \(x\) 的 \(sa_i\) 和 \(rk_i\) 推长度为 \(2x\) 的 \(sa'_i\) 和 \(rk'_i\) 。
\(sa'_i\) 为
- 以前 \(x\) 个字符为第一关键字。
后 \(x\) 个字符为第二关键字。
排序的结果。
理解即可
rpt(i,1,n)sa[i]=i;
//此时 sa 的值不是正确的意义但仍可以保证正确
rpt(i,1,n)rk[i]=s[i];
//因为 rk 的值是正确的
for(int w=1;w<n;w<<=1){
//w 为上一组的字符串长度
sort(sa+1,sa+1+n,[&](int x,int y){
return rk[x]==rk[y]?rk[x+w]<rk[y+w]:rk[x]<rk[y];
});
//排名离散化
int now=0;
rpt(i,1,n){
if(rk[sa[i]]!=rk[sa[i-1]]||rk[sa[i]+w]!=rk[sa[i-1]+w])now++;
tmprk[sa[i]]=now;
//不能直接修改 rk
}
rpt(i,1,n)rk[i]=tmprk[i];
}
\(O(nlogn)\)做法
我们考虑到 \(rk_i\) 的值域并不会超过 \(n\) 。
所以我们把排序换成基数排序即可。
优化前
int v=0;
rpt(i,1,n)v=max(v,(int)s[i]);
// v 表示基数排序值域
rpt(i,1,n)rk[i]=s[i];
rpt(i,0,v)t[i]=0;
rpt(i,1,n)t[rk[i]]++;
rpt(i,1,v)t[i]+=t[i-1];
rpt(i,1,n)sa[t[rk[i]]--]=i;
//此时 sa 的值需要表示正确的意义
for(int w=1;w<n;w<<=1){
//w 为上一组的字符串长度
rpt(i,0,v)t[i]=0;
rpt(i,1,n)t[rk[i+w]]++;
//以 rk[i+w] 为关键字
rpt(i,1,v)t[i]+=t[i-1];
rpt(i,1,n)sa[t[rk[i+w]]--]=i;
//第二关键字排序
rpt(i,0,v)t[i]=0;
rpt(i,1,n)id[i]=sa[i];
rpt(i,1,n)t[rk[id[i]]]++;
//以 rk[i] 为关键字
rpt(i,1,v)t[i]+=t[i-1];
tpr(i,n,1)sa[t[rk[id[i]]]--]=id[i];
//倒序枚举排名以保证相同大小按原顺序排序
//sa 会被改变所以用 id 存一下
//第一关键字排序
//排名离散化
int now=0;
rpt(i,1,n){
if(rk[sa[i]]!=rk[sa[i-1]]||rk[sa[i]+w]!=rk[sa[i-1]+w])now++;
tmprk[sa[i]]=now;
//不能直接修改 rk
}
v=now;
rpt(i,1,n)rk[i]=tmprk[i];
}
其实我们会发现上面代码有很多优化空间。
- \(1.\) 在上一层时 \(rk_i\) 已经有序,所以不需要按第二关键字排序,只需考虑第二关键字为空的情况。
\(2.\) 当 \(rk_i\) 已经互不相同时,说明已经比出大小,直接结束即可。
优化后
for(int w=1;w<n;w<<=1){
//w 为上一组的字符串长度
int top=0;
rpt(i,n-w+1,n)id[++top]=i;
rpt(i,1,n)if(sa[i]-w>0)id[++top]=sa[i]-w;
rpt(i,1,n)sa[i]=id[i];
//第二关键字其实只需要考虑空串
//其他非空集已经有序
rpt(i,0,v)t[i]=0;
rpt(i,1,n)id[i]=sa[i];
rpt(i,1,n)t[rk[id[i]]]++;
rpt(i,1,v)t[i]+=t[i-1];
tpr(i,n,1)sa[t[rk[id[i]]]--]=id[i];
//第一关键字排序
//排名离散化
int now=0;
rpt(i,1,n){
if(rk[sa[i]]!=rk[sa[i-1]]||rk[sa[i]+w]!=rk[sa[i-1]+w])now++;
tmprk[sa[i]]=now;
//不能直接修改 rk
}
v=now;
rpt(i,1,n)rk[i]=tmprk[i];
if(v==n)break;
}
\(height\) 数组
\(height\) 数组(以下称 \(he_i\) ) 是后缀数组解决任意两个后缀的最长前缀的手段。
定义 \(he_i\) 为 \(sa_i\) 和 \(sa_{i-1}\) 最长公共前缀长度。
\(he_i\) 的求法
重要性质:
\(he_{rk_i} \ge he_{rk_{i-1}}-1\)
证明:
\(he_{rk_{i-1}} = (sa_{rk_{i-1}},sa_{rk_{i-1}-1})\) 。
\(he_{rk_i} = (sa_{rk_i},sa_{rk_i-1})\) 。
在排好序的后缀中:
设 \(sa_{rk_{i-1}}=aAD\) , \(sa_{rk_{i-1}-1}=aAB\) 。
则有 \(sa_{rk_i}=AD\) 。
因为后缀 \(AD\) 必然存在。
所以 \(sa_{rk_i-1}=AC\) 且满足 \(D > C \geq B\) 。
即至多会减少 \(a\) 一个字符。