Preface

我们希望将一个长度为n的字符串S的所有后缀按照字典序排序。
因为每个后缀的长度不相同,因此不会出现相等的情况
目标:求出两个数组SA[]和Rank[],SA[i]表示排第i名的是谁,Rank[i]则是以i这个位置开头的后缀排第几名
一般后缀排序有两种方法,倍增和DC3,这里只讲好打方便常用的倍增。DC3我并不会(据说DC3某些时候跑的比倍增还要慢)

进入正题

SA和Rank

我们设suf(i)表示第i个位置开头的后缀(即S[i..n])。
对于每个suf(i),定义它长度为k的前缀为suf(i,k)(即S[i..i+k-1])
显然有几个性质

suf(i,2k)<suf(j,2k) s u f ( i , 2 k ) < s u f ( j , 2 k ) 当且仅当suf(i,k)<suf(j,k) s u f ( i , k ) < s u f ( j , k ) 或suf(i,k)=suf(j,k) s u f ( i , k ) = s u f ( j , k ) 并且suf(i+k,k)<suf(j+k,k) s u f ( i + k , k ) < s u f ( j + k , k )
suf(i,2k)=suf(j,2k) s u f ( i , 2 k ) = s u f ( j , 2 k ) 当且仅当suf(i,k)=suf(j,k) s u f ( i , k ) = s u f ( j , k ) 并且suf(i+k,k)=suf(j+k,k) s u f ( i + k , k ) = s u f ( j + k , k )

设数组SAk[],Rankk[] S A k [ ] , R a n k k [ ] 表示在只排序每个后缀长度为k的前缀条件下的SA,Rank
那么完全可以利用Rank来比较大小

然而,在k长度前缀下可能有完全相同的字符串,那么他们的Rank_k就是相等的

于是每个suf(i,2k),都有两个关键字Rankk[i] R a n k k [ i ] ,Rankk[i+k] R a n k k [ i + k ]

可以利用双关键字基数排序O(n)求出SA2k[] S A 2 k [ ] ,Rank2k[] R a n k 2 k [ ] ,再到4k,8k,16k,最后当k大于等于n了,SAk S A k 和Rankk R a n k k 就是SA和Rank了。那么排序就结束了(显然超出部分并不影响)

当然,初始时你需要对每一个字符做一遍排序,相当于k=1
事实上,整个倍增求SA的算法的核心就在于双关键字基数排序,它的实现特别的玄妙,

算法流程

首先,根据计算好的SA_k,Rank_k求出以suf(i+k,k)为关键字将i排序的数组s2_k[]

然后我们用计数排序的思想,开一个计数数组ct,ct[i]表示Rankk[]<=i R a n k k [ ] <= i 的后缀的数量,ct[i]-ct[i-1]为Rankk[]=i R a n k k [ ] = i 的后缀的数量(因为可以重复,所以可能有多个)

我们从n到1访问s2_k[i],那么新的第ct[Rankk[s2k[i]]] c t [ R a n k k [ s 2 k [ i ] ] ] 名就是s2_k[i],也就是说SA2k[ct[Rankk[s2k[i]]]]=s2k[i] S A 2 k [ c t [ R a n k k [ s 2 k [ i ] ] ] ] = s 2 k [ i ] (是不是有点绕。。)
然后ct[Rankk[s2k[i]]]−− c t [ R a n k k [ s 2 k [ i ] ] ] − − 。

这个过程保证了第一关键字小的一定排名更小,第一关键字相同时第二关键字小的排名更小
之后,我们只需要通过计算好的新的SA_2k来计算Rank_2k
结合代码来理解一下

后缀排序部分代码

#define fo(i,a,b) for(i=a;i<=b;i++)
#define fod(i,a,b) for(i=a;i>=b;i--)
void make()
{
memset(ct,0,sizeof(ct));
int i,j,k,mx;
fo(i,1,n) ct[rank[i]=st[i]]++;
//这里用的是计数排序,为了方便,直接用ASCII码来比大小
fo(i,1,m) ct[i]+=ct[i-1];
fod(i,n,1) SA[ct[rank[i]]--]=i;//求出SA1
mx=m;
for(k=1,j=0;j<n;j*=2,mx=j)
//这里mx表示最大的Rank值,也就是不同的sf(i,k)数目,如果每一个字符串都不同,再排也就没有意义了,直接退出。
{
int p=0;
fo(i,n-k+1,n) s2[++p]=i;
fo(i,1,n) if(SA[i]>k) s2[++p]=SA[i]-k;
//第二关键字直接用上一次的SA计算,s2表示按照第二关键字排序的sf(i,2k)
memset(ct,0,sizeof(ct));
fo(i,1,n) ct[rank[s2[i]]]++;
fo(i,1,mx) ct[i]+=ct[i-1];
fod(i,n,1) SA[ct[rank[s2[i]]]--]=s2[i];
//这里必须用downto,因为要把s2排在后面的放的尽量靠后(所以要--)
r1[SA[1]]=j=1;
fo(i,2,n) r1[SA[i]]=(rank[SA[i-1]]==rank[SA[i]]&&rank[SA[i-1]+k]==rank[SA[i]+k])?j:++j;
//求新一轮的Rank,j代表当前到第几名,也就是不同的rank个数
fo(i,1,n) rank[i]=r1[i];
}
}

由于长度是2的幂次倍增上去的,因此只会做log n层,每层的复杂度都是O(n)的
因此总复杂度为O(n log n)

最长公共前缀(LCP)

然而,往往光有SA和Rank是并不够的,我们还需要一个height数组

设lcp(i,j)表示suf(i)和suf(j)的最长公共前缀长度 ,LCP(i,j)表示lcp(SA[i],SA[j])的长度(注意区分大小写,大写的表示第i名和第j名的lcp)。

设height[i]=LCP(i−1,i),且令height[1]=0
首先,十分显然的,对于任意i<=k<j i <= k < j ,有LCP(i,j)<=LCP(i,k) L C P ( i , j ) <= L C P ( i , k ) 。( 即字典序排名更接近后缀的lcp更长)

有定理:

对于任意1<=i<k<j<=n 1 <= i < k < j <= n ,LCP(i,j)=min(LCP(i,k),LCP(k,j)) L C P ( i , j ) = m i n ( L C P ( i , k ) , L C P ( k , j ) ) 均成立

这个也比较显然,随便举个例子画一画就可以看出。

上面的式子可以证明LCP定理、

对于i<k<=j,LCP(i,j)=min(height[k]) i < k <= j , L C P ( i , j ) = m i n ( h e i g h t [ k ] ) 。

有了这个东西,求两个后缀的lcp转化为了区间最小值,我们就可以用RMQ之类的算法快速完成了
(感性理解一下)
具体证法较为复杂,见

​http://baike.baidu.com/link?url=nYOXOZ0cAcRFHtSczK6JI05hTsSRlQTiKyvPds90yB7CjooOQfeNI3W1nVk04mNd8-lr07ehEnZICh8R_LgOg_​

问题在于如何快速求height,当然可以二分+Hash,但这样太蠢了
我们有O(n)的做法
设h[i]=height[Rank[i]],也就是suf(i)和suf(i)的前一名的lcp。
显然h[SA[i]]=height[i]

有定理

若i>1且Rank[i]>1,则有h[i]≥h[i−1]−1

证明如下:

设j=SA[Rank[i]−1] j = S A [ R a n k [ i ] − 1 ] (suf(i)的前一名),k=SA[Rank[i−1]−1] k = S A [ R a n k [ i − 1 ] − 1 ] (即suf(i-1)的前一名)
则h[i]=lcp(j,i),h[i−1]=lcp(k,i−1) h [ i ] = l c p ( j , i ) , h [ i − 1 ] = l c p ( k , i − 1 )
若h[i−1]≤1 h [ i − 1 ] ≤ 1 ,定理显然成立,因为h[i]≥0≥h[i−1]−1 h [ i ] ≥ 0 ≥ h [ i − 1 ] − 1
若h[i−1]>1 h [ i − 1 ] > 1 ,即lcp(k,i−1)>1 l c p ( k , i − 1 ) > 1
对于x,y<N x , y < N ,如果有lcp(x,y)≥1 l c p ( x , y ) ≥ 1 ,则suf(x)和suf(y)的第一个字符相同
那么suf(x)<suf(y) s u f ( x ) < s u f ( y ) 等价于suf(x+1)<suf(y+1),lcp(x+1,y+1)=lcp(x,y)−1 s u f ( x + 1 ) < s u f ( y + 1 ) , l c p ( x + 1 , y + 1 ) = l c p ( x , y ) − 1
我们有lcp(k,i−1)>1,suf(k)<suf(i−1) l c p ( k , i − 1 ) > 1 , s u f ( k ) < s u f ( i − 1 )
因此suf(k+1)<suf(i) s u f ( k + 1 ) < s u f ( i ) (即Rank[k+1]≤Rank[i]−1 R a n k [ k + 1 ] ≤ R a n k [ i ] − 1 ),h[i−1]=lcp(k,i−1)=lcp(k+1,i)+1 h [ i − 1 ] = l c p ( k , i − 1 ) = l c p ( k + 1 , i ) + 1
又Rank[j]=Rank[i]−1 R a n k [ j ] = R a n k [ i ] − 1 ,联系之前的引理可得h[i]=lcp(j,i)≥lcp(k+1,i)=h[i−1]−1 h [ i ] = l c p ( j , i ) ≥ l c p ( k + 1 , i ) = h [ i − 1 ] − 1
得证!

求Height

有了这个性质,我们就可以有O(N) O ( N ) 的方法求出height h e i g h t

对于每一个h[i] h [ i ] ,我们直接从h[i−1]−1 h [ i − 1 ] − 1 的长度开始暴力判断lcp(sf(i),sf(SA[Rank[i]−1])) l c p ( s f ( i ) , s f ( S A [ R a n k [ i ] − 1 ] ) )

求出h h 后再根据heightheight和h h 的关系式求出heightheight

每次判断看似是O(N)的,然而,h最多-1 N次,右移最多是2N次,总复杂度仍是O(N)

下面是代码,真的非常短

代码

void findh()
{
height[1]=0;
int i=0,j;
fo(i,1,n)
{
if (rank[i]==1) continue;
j=max(height[rank[i-1]]-1,0);
while (st[i+j]==st[SA[rank[i]-1]+j]) j++;
height[rank[i]]=j;
}
}

至此,大功告成
附上SA S A 模版


这里有一道SA S A 的应用题,大家可以做一做
JZOJ[1598]文件修复,下面附上链接的题意和讲解