后缀数组(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] \}\)

posted @ 2023-12-26 21:47  yisiwunian  阅读(17)  评论(0编辑  收藏  举报