后缀数组(SA)

发布时间 2023-12-26 21:47:15作者: yisiwunian

终于刷完网络流后准备继续做sa,发现自己忘完了,于是来写个博客。

应用

\(O(nlogn)\)将字符串后缀排序,以找到优美的性质

概念

两个数组:\(sa\)\(rk\)
\(sa_i\)表示将字符串后缀排序后,排名为\(i\)的后缀的开头字母在原串的位置
\(rk_i\)表示后缀\(i\)的排名
满足性质: \(sa[rk[i]]=rk[sa[i]]=i\)

一定一定要清楚区分两个数组,因为经常会嵌套使用,如\(sa[cnt[rk[id[i]]]--]=id[i]\)。意义不够清楚的话,就很难理解代码

oi-wiki的字符串太长了,不方便手模,所以手绘了一个

原理

主要是倍增的思想

对于两个串\(S\)\(T\),其中\(S=s1+s2,T=t1+t2. s1,s2,t1,t2\)长度相等。已知\(s1\)\(t1\)\(s2\)\(t2\)的大小关系,可知\(S\)\(T\)的大小关系,以前串为第一关键字,后串为第二关键字比较即可。

当排名各不相同时跳出。得到\(rk\)数组,由\(sa[rk[i]]=i\)可得\(sa\)

代码

前方大量注释来袭

int p,rk[MAX],sa[MAX],cnt[MAX],id[MAX],ork[MAX],s[MAX];
inline bool cmp(int x,int y,int w){
    return ork[x]==ork[y]&&ork[x+w]==ork[y+w];
};
inline void SA(int n){
	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;--i)  sa[cnt[rk[i]]--]=i;
//O(n)基数排序
//与上图的串为例,rk 97 97 98 97 98,sa 1 2 4 3 5
    for(int j=1;p!=n;j<<=1,m=p){
//p为排名数组中的最大值,当p==n时排序完成
//j为上次排序长度
        p=0;
//当前串已排好长度为j的序,即第一关键字是有序的,那么只需排序第二关键字。实质是将超出字符串范围的后缀放到同第一关键字的最前,即一起放在基数排序的前面
        for(int i=n;i>n-j;--i)  id[++p]=i;
//没超出的依次放入
        for(int i=1;i<=n;++i)
            if(sa[i]>j)  id[++p]=sa[i]-j;
//第二次排序中,id 5 1 3 2 4
        memset(cnt,0,(m+1)*sizeof(int));
        for(int i=1;i<=n;++i)  cnt[rk[id[i]]]++;
        for(int i=1;i<=m;++i)  cnt[i]+=cnt[i-1];
        for(int i=n;i;--i)  sa[cnt[rk[id[i]]]--]=id[i];
//id中5在3前,所以基数排序中从后往前扫,从后往前放,于是sa中5在3前。sa 1 2 4 5 3
//此时只排了第二关键字为0的后缀,sa不完全是此次排序后的sa
        memcpy(ork,rk,sizeof(rk));p=0;
        for(int i=1;i<=n;++i)
            rk[sa[i]]=cmp(sa[i],sa[i-1],j)?p:++p;
//离散化,第一或第二关键字不一样就和上一个区分开,更新rk和p,如上图第二次排序rk 1 2 4 2 3,p=4
    }for(int i=1;i<=n;++i)  sa[rk[i]]=i;
//根据rk更新最终sa
}

代码不好背也不好理解,可以多手模或者干脆所学几遍