参考:
后缀数组就是将字符串所有后缀排序后的数组,设字符串为S,令后缀Suffix(i)表示S[i..len(S)]。用两个数组记录所有后缀的排序结果:
- Rank[i]记录Suffix(i)排序后的序号,即Suffix[i]在所有后缀中是第Rank[i]小的后缀
- SA[i]记录第i位后缀的首字母位置,即Suffix[SA[i]]在所有后缀中是第i小的后缀
方法是倍增法,定义一个字符串的k-前缀为该字符串的前k个字符组成的串,关于在k-后缀上的定义Suffix(k,i)、SA[k,i]和Rank[k,i]类似于前,则有
- 若Rank[k,i]=Rank[k,j]且Rank[k,i+k]=Rank[k,j+k],则Suffix[2k,i]=Suffix[2k,j]
- 若Rank[k,i]=Rank[k,j]且Rank[k,i+k]<Rank[k,j+k],则Suffix[2k,i]<Suffix[2k,j]
- 若Rank[k,i]<Rank[k,j],则Suffix[2k,i]<Suffix[2k,j]
于是求出了所有后缀的排序,有什么用呢?主要是用于求它们之间的最长公共前缀(Longest Common Prefix,LCP)
令LCP(i,j)为第i小的后缀和第j小的后缀(也就是Suffix(SA[i])和Suffix(SA[j]))的最长公共前缀的长度,则有如下两个性质:
- 对任意i<=k<=j,有LCP(i,j) = min(LCP(i,k),LCP(k,j))
- LCP(i,j)=min(i<k<=j)(LCP(k-1,k))
令height[i]=LCP(i-1,i),即height[i]代表第i小的后缀与第i-1小的后缀的LCP,则求 LCP(i,j)就等于求height[i+1]~height[j]之间的RMQ ,套用RMQ算法就可以了,复杂度是预处理O(nlogn),查询O(1)
然后height的求法要用到另一个数组:令h[i]=height[Rank[i]],即h[i]表示Suffix(i)的height值(同时height[i]就表示Suffix(SA[i])的height值),则有height[i]=h[SA[i]]
然后h[i]有个性质:
- h[i] >= h[i-1]-1
用这个性质我们在计算h[i]的时候进行后缀比较时只需从第h[i-1]位起比较,从而总的比较的复杂度是O(n),也就是说h数组在O(n)的时间内解决了。求出了h数组,根据关系式height[i]=h[SA[i]]可以在O(n)时间内求出height数组,于是可以在O(n)时间内求出height数组,从而整个LCP问题就解决了^_^
然后后缀数组的应用就是利用它的LCP在需要字符串比较时降低复杂度。同时由于后缀数组的有序性可以很方便地使用二分
后缀数组就是将字符串所有后缀排序后的数组,设字符串为S,令后缀Suffix(i)表示S[i..len(S)]。用两个数组记录所有后缀的排序结果:
- Rank[i]记录Suffix(i)排序后的序号,即Suffix[i]在所有后缀中是第Rank[i]小的后缀
- SA[i]记录第i位后缀的首字母位置,即Suffix[SA[i]]在所有后缀中是第i小的后缀
然后就是怎么快速求所有后缀的顺序了,其中的关键是如何减少两个后缀比较的复杂度
方法是倍增法,定义一个字符串的k-前缀为该字符串的前k个字符组成的串,关于在k-后缀上的定义Suffix(k,i)、SA[k,i]和Rank[k,i]类似于前,则有
- 若Rank[k,i]=Rank[k,j]且Rank[k,i+k]=Rank[k,j+k],则Suffix[2k,i]=Suffix[2k,j]
- 若Rank[k,i]=Rank[k,j]且Rank[k,i+k]<Rank[k,j+k],则Suffix[2k,i]<Suffix[2k,j]
- 若Rank[k,i]<Rank[k,j],则Suffix[2k,i]<Suffix[2k,j]
这样就能在常数时间内比较Suffix(2^k, i)之间的大小,从而对Suffix(2^k,i)时行排序,最后当2^k>n时,Suffix(2^k, i)之间的大小即为所有后缀之间的大小
于是求出了所有后缀的排序,有什么用呢?主要是用于求它们之间的最长公共前缀(Longest Common Prefix,LCP)
令LCP(i,j)为第i小的后缀和第j小的后缀(也就是Suffix(SA[i])和Suffix(SA[j]))的最长公共前缀的长度,则有如下两个性质:
- 对任意i<=k<=j,有LCP(i,j) = min(LCP(i,k),LCP(k,j))
- LCP(i,j)=min(i<k<=j)(LCP(k-1,k))
第一个性质是显然的,它的意义在于可以用来证明第二个性质。第二个性质的意义在于提供了一个将LCP问题转换为RMQ问题的方法:
令height[i]=LCP(i-1,i),即height[i]代表第i小的后缀与第i-1小的后缀的LCP,则求LCP(i,j)就等于求height[i+1]~height[j]之间的RMQ,套用RMQ算法就可以了,复杂度是预处理O(nlogn),查询O(1)
然后height的求法要用到另一个数组:令h[i]=height[Rank[i]],即h[i]表示Suffix(i)的height值(同时height[i]就表示Suffix(SA[i])的height值),则有height[i]=h[SA[i]]
然后h[i]有个性质:
- h[i] >= h[i-1]-1
用这个性质我们在计算h[i]的时候进行后缀比较时只需从第h[i-1]位起比较,从而总的比较的复杂度是O(n),也就是说h数组在O(n)的时间内解决了。求出了h数组,根据关系式height[i]=h[SA[i]]可以在O(n)时间内求出height数组,于是可以在O(n)时间内求出height数组,从而整个LCP问题就解决了^_^
然后后缀数组的应用就是利用它的LCP在需要字符串比较时降低复杂度。同时由于后缀数组的有序性可以很方便地使用二分
于是总结一下要点:
- 利用倍增算法在O(nlogn)的时间内对后缀数组进行排序
- 利用h数组的性质在O(n)的时间内求出储存排序后相邻后缀间的LCP数的组height
- 利用LCP的性质将平凡LCP问题转化为height数组上的RMQ问题
理解:
有关后缀数组的Rank, SA性质: Randk [ SA[ i ] ] = i,即后缀i排名为k时,则排名为k的后缀对应后缀i;同样 SA[ Rank[ i ] ] = i.
h[ i ] 与 h[ i-1 ] 之间的关系: 设suffix(k)是排在suffix(i-1)前一名的后缀,则它们的最长公共前缀是h[i-1]。那么suffix(k+1)将排在suffix(i)的前面(这里要求h[i-1]>1,如果h[i-1]≤1,原式显然成立)并且suffix(k+1)和suffix(i)的最长公共前缀是h[i-1]-1,所以suffix(i)和在它前一名的后缀的最长公共前缀至少是h[i-1]-1。按照h[1],h[2],……,h[n]的顺序计算,并利用h数组的性质,时间复杂度可以降为O(n)。注意:若suffix(k)是suffix(i-1)前一名的后缀,则suffix(k+1)一定排在suffix(i)前面。
下面依次说明 SA 、Rank、h、height的实际意义:
SA[i]: 第i名的索引, SA中存储的是索引。suffix( SA[i] ) < suffix( SA[i+1] )。你是谁?
Rank[i]: 后缀str[i...N]的名次,即Rank中存储的是排名。存储 suffix(i)的名次。你排第几?
LCP(i, j): suffix( SA[i] )和 suffix( SA[j] ) 的公共前缀,即第i小后缀和第j小后缀的公共前缀。
height[ i ]: LCP(i-1, i)即 suffix( SA[i-1] )和suffix( SA[i] )的公共前缀,排名为i的后缀与其第 i-1 名的公共前缀。
h[ i - 1]: h[i-1] = LCP( Rank[i-1] -1, Rank[i-1] ) 即 suffix( SA[Rank[i-1] -1] ) 和 suffix( i-1)的公共前缀。设 Rank[i-1]-1 = k, 则suffix( SA[k] )为 suffix( i-1 )的前一名。
h[ i ]: h[i] = height[ Rank[i] ] = LCP( Rank[i] -1, Rank[i] ) 。由于 suffix( i )与suffix(i-1)相比,少了suffix(i-1)的首字母,而suffix( k+ 1)与suffix(k-1)相比,也少了一个首字母,故suffix( k + 1 )排在suffix( i )前面, 并且它们之间的公共前缀为 h[i-1]-1。如果Rank[k+1]-Rank[i]>1,则说明 suffix(k+1)与suffix(i)之间还有别的后缀,h[i]>h[i-1]-1,否则,h[i] = h[i-1]-1。
下面依次实现 SA、Rank、h、height各分部:
Rank:
倍增算法的主要思路是:用倍增的方法对每个字符开始的长度为2^k的子字符串进行排序,求出排名,即rank值。k从0开始,每次加1,当2^k大于n以后,每个字符开始的长度为2k的子字符串便相当于所有的后缀。并且这些子字符串都一定已经比较出大小,即rank值中没有相同的值,那么此时的rank值就是最后的结果。每一次排序都利用上次长度为2^(k-1)的字符串的rank值,那么长度为2^k的字符串就可以用两个长度为2k-1的字符串的排名作为关键字表示,然后进行基数排序,便得出了长度为2k的字符串的rank值。