后缀数组(后缀排序)SA
前置芝士:基数排序
算法简介
后缀数组可以将一个字符串的后缀按照字典序排序(啊对,就这么多
实现
定义数组sa,rk
\(sa_i\)表示排名为i的后缀的起点
\(rk_i\)表示以i为起点的后缀排名
显然,rk数组于sa数组是互逆的,即\(sa_{rk_i}=i\)
后缀数组的主要部分就是在求解sa数组和rk数组
我们可以考虑倍增的求法,枚举k,当序列没有排序完成时,我们用rk和sa数组分别表示对应位置开始的前\(2^k\)个字符的排名和位置(空字符默认最小
发现,当我们考虑完k-1的情况后,对于k情况下的两个字符串i,j,只用先比较\(rk_i\)和\(rk_j\),再比较\(rk_{i+2^{k-1}}\)和\(rk_{j+2^{k-1}}\)的大小即可
按照两个关键字排序后,再按照新的关键字排序,这可以联想到基数排序,我们就可以用基数排序优化倍增过程,在O(nlogn)的时间复杂度内求解出rk和sa数组
具体的排序过程可以看以下代码
点击查看代码
struct node_sufarr{
ll rk[MAXN],_rk[MAXN],sa[MAXN];//_rk存前一层的rk值
ll siz;
ll it[MAXN],id[MAXN],_id[MAXN];//id和_id维护桶
char c[MAXN];
bool cmp(ll a,ll b,ll w){//比较两个位置排名是否相同
return _rk[a]==_rk[b]&&_rk[a+w]==_rk[b+w];
}
void build(){
ll m=1<<10,p=0;
for(int i=1;i<=siz;i++)it[rk[i]=c[i]]++;
for(int i=1;i<=m;i++)it[i]+=it[i-1];
for(int i=1;i<=siz;i++)sa[it[rk[i]]--]=i;
for(int w=1;;w<<=1,m=p,p=0){//w为上午所说的2^{k-1}
for(int i=siz;i>siz-w;i--)id[++p]=i;//空字符最小
for(int i=1;i<=siz;i++)if(sa[i]>w)id[++p]=sa[i]-w;//按照第一关键字即后2^{k-1}个字符排序(因为优先级弱于第二关键字,所以我们先将第一关键字的顺序确定好后按第二关键字排序,这样最后的排序就是以第二关键字优先,具体可以看基数排序过程
for(int i=1;i<=m;i++)it[i]=0;//清空
for(int i=1;i<=siz;i++)it[_id[i]=rk[id[i]]]++;
for(int i=1;i<=m;i++)it[i]+=it[i-1];//桶指针
for(int i=siz;i;i--)sa[it[_id[i]]--]=id[i];//更新sa
for(int i=1;i<=siz;i++)_rk[i]=rk[i],rk[i]=0;//更新_rk,清空rk
p=0;
for(int i=1;i<=siz;i++)rk[sa[i]]=cmp(sa[i-1],sa[i],w)?p:++p;//判断相邻两个位置是否完全相同,如果相同排名也相同
if(w>=siz)break;//此处也可换成(p>=siz)即已经排序完成
}
for(int i=1;i<=siz;i++)cout<<sa[i]<<" ";
}
}
洛谷模板
水题练手:P4051 [JSOI2007] 字符加密
应用
定义ht数组,\(ht_i\)表示排名为i的字符串于排名为i-1的字符串的最长公共前缀
一个很显然的性质,两个排名越靠近的后缀,其最长公共前缀也就越长,可以手模两个字符串
所以,当我们求解出ht数组后,我们查询任意两个后缀的最长公共前缀,只需要找到他们的排名l,r,最长公共前缀即为\(\underset{l<i \leq{r}}{min} ht_i\)
那我们怎么求解这个ht数组呢
ht数组有个很重要的性质,也就是我们考虑原串的每个位置i,都有\(ht_{rk_i}>=ht_{rk_{i-1}}-1\)
证明比较复杂但也很显然,可以看其他dalao的证明过程或者手模两个串就可以自己发现
这样我们就可以线性的求解ht数组,代码也很短
点击查看代码
ll k=0;
for(int i=1;i<=n;i++){
if(k)k--;
while(c[i+k]==c[sa[rk[i]-1]+k])k++;
ht[rk[i]]=k;
}

浙公网安备 33010602011771号