终于刷完网络流后准备继续做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
}
代码不好背也不好理解,可以多手模或者干脆所学几遍