后缀数组(SA)
终于刷完网络流后准备继续做sa,发现自己忘完了,于是来写个博客。
应用
用\(O(nlogn)\)将字符串后缀排序,以找到优美的性质
概念
两个数组:\(sa\)和\(rk\)
\(sa_i\)表示将字符串后缀排序后,排名为\(i\)的后缀的开头字母在原串的位置
\(rk_i\)表示后缀\(i\)的排名
满足性质: \(sa[rk[i]]=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
}
代码不好背也不好理解,可以多手模或者干脆所学几遍
height数组
LCP
\(lcp(i,j)\)表示后缀i和后缀j的最长公共前缀
height
\(height[i]=lcp(sa[i],sa[i-1])\),即排名为i的后缀与前一名后缀的最长公共前缀
\(height[1]=0\)
lemma
\(height[rk[i]] \ge height[rk[i-1]]-1\)
证明
当$height[rk[i-1]] \le 1$时,$height[rk[i]] \ge 0 \ge height[rk[i-1]]-1 $当\(height[rk[i-1]]>1\)时,即\(lcp(i-1,sa[rk[i-1]-1])>1\)
后缀i是后缀i-1的后缀,设后缀i-1为"\(aAB\)",则后缀i为"\(AB\)"。其中,\(a\)为\(s[i-1]\),\(AB\)代表一个字符串
设后缀\(sa[rk[i-1]-1]\)为"\(aAC\)",其中\(C<B\)且\(C\)可以为空串,则它与后缀i-1的lcp为\(aA\)(长度\(height[rk[i-1]]\))。它又有后缀\(AC\),与后缀i至少有\(A\)(长度\(height[rk[i-1]]-1\))的公共前缀。
又因为后缀i与它排名相邻的字符串相似度最高,所以\(height[rk[i]]>=|A|=height[rk[i-1]]-1\)
由以上引理可以暴力移动k(后缀i与排名上一名的lcp),其下降不超过n次,最大不超过n,所以最多移动2n次,即复杂度\(O(n)\)
代码
inline void getth(){
int k=0;
for(int i=1;i<=n;++i){
if(rk[i]==1) continue;
if(k) --k;
int j=sa[rk[i]-1];
while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k]) ++k;
height[rk[i]]=k;
}
}
定理
两子串最长公共前缀\(lcp(sa[i],sa[j])=min \{ height[i+1 \dots j] \}\)