后缀数组

基本定义


子串字符串S 的子串r[i..j],i≤j,表示r 串中从i 到j 这一段,也就是顺次排列r[i],r[i+1],...,r[j]形成的字符串。


后缀后缀是指从某个位置i 开始到整个串末尾结束的一个特殊子串。字符串r 的从第i 个字符开始的后缀表示为Suffix(i) , 也就是Suffix(i)=r[i..len(r)]。


大小比较关于字符串的大小比较,是指通常所说的“字典顺序”比较,也就是对于两个字符串u、v,令i 从1 开始顺次比较u[i]和v[i],如果u[i]=v[i]则令i 加1,否则若u[i]<v[i]则认为u<v,u[i]>v[i]则认为u>v(也就是v<u),比较结束。如果i>len(u)或者i>len(v)仍比较不出结果,那么若len(u)<len(v) 则认为u<v , 若len(u)=len(v) 则认为u=v , 若len(u)>len(v)则u>v。
从字符串的大小比较的定义来看,S 的两个开头位置不同的后缀u 和v 进行比较的结果不可能是相等,因为u=v 的必要条件len(u)=len(v)在这里不可能满足。


后缀数组
后缀数组SA 是一个一维数组,它保存1..n 的某个排列SA[1],SA[2],……,SA[n],并且保证Suffix(SA[i]) < Suffix(SA[i+1]),1≤i<n。也就是将S 的n 个后缀从小到大进行排序之后把排好序的后缀的开头位置顺次放入SA 中。


名次数组
名次数组Rank[i]保存的是Suffix(i)在所有后缀中从小到大排列的“名次”。
简单的说,后缀数组是“排第几的是谁?”,名次数组是“你排第几?”。容易看出,后缀数组和名次数组为互逆运算。




设字符串的长度为n。为了方便比较大小,可以在字符串后面添加一个字符,这个字符没有在前面的字符中出现过,而且比前面的字符都要小。在求出名次数组后,可以仅用O(1)的时间比较任意两个后缀的大小。在求出后缀数组或名次数组中的其中一个以后,便可以用O(n)的时间求出另外一个。任意两个后缀如果直接比较大小,最多需要比较字符n 次,也就是说最迟在比较第n 个字符时一定能分出“胜负”。


2倍增算法


倍增算法的主要思路是:用倍增的方法对每个字符开始的长度为2^k 的子字符串进行排序,求出排名,即rank 值。k 从0 开始,每次加1,当2^k 大于n 以后,每个字符开始的长度为2^k 的子字符串便相当于所有的后缀。并且这些子字符串都一定已经比较出大小,即rank 值中没有相同的值,那么此时的rank 值就是最后的结果。每一次排序都利用上次长度为2k-1 的字符串的rank 值,那么长度为2^k 的字符串就可以用两个长度为2^(k-1) 的字符串的排名作为关键字表示,然后进行基数排序,便得出了长度为2^k 的字符串的rank 值。以字符串“aabaaaab”为例,整个过程如图2 所示。其中x、y 是表示长度为2^k 的字符串的两个关键字。





最长公共前缀


height 数组定义height[i]=suffix(sa[i-1])和suffix(sa[i])的最长公共前缀,也就是排名相邻的两个后缀的最长公共前缀。那么对于j 和k,不妨设rank[j]<rank[k],则有以下性质:
suffix(j) 和suffix(k) 的最长公共前缀为height[rank[j]+1],height[rank[j]+2], height[rank[j]+3], … ,height[rank[k]]中的最小值。
例如,字符串为“aabaaaab”,求后缀“abaaaab”和后缀“aaab”的最长公共前缀,如图4所示:






代码实现:

/*
倍增算法  O(n*logn) 
待排序数组长度为n,放在0~n-1中,在最后面补一个0 
da(r,sa,n+1,m);//注意是n+1; 
例如:
n = 8; 
num[]   = { 1, 1, 2, 1, 1, 1, 1, 2, $ };注意num最后一位为0,其他大于0 
rank[]  = { 4, 6, 8, 1, 2, 3, 5, 7, 0 };rank[0~n-1]为有效值,rank[n]必定为0无效值 
sa[]    = { 8, 3, 4, 5, 0, 6, 1, 7, 2 };sa[1~n]为有效值,sa[0]必定为n是无效值 
height[]= { 0, 0, 3, 2, 3, 1, 2, 0, 1 };height[2~n]为有效值 
*/


#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
#define N 101000

int wa[N],wb[N],wv[N],ws[N];
int sa[N];      //排第i的后缀的开头位置,sa[1~n]为有效值,sa[0]必定为n是无效值
int rank[N];    //开头位置为i的后缀排第几,rank[0~n-1]为有效值,rank[n]必定为0无效值
                //后缀数组和名次数组为互逆运算,rank[sa[i]]==i
int height[N];  //height[i]=suffix(sa[i-1])和suffix(sa[i])的最长公共前缀,
                //也就是排名相邻的两个后缀的最长公共前缀。
                //height[2~n]为有效值

int cmp(int *r,int a,int b,int l){
    return r[a]==r[b]&&r[a+l]==r[b+l];
}



void da(int *r,int *sa,int n,int m){
    int i,j,p,*x=wa,*y=wb,*t;
    for(i=0;i<m;++i) ws[i]=0;
    for(i=0;i<n;++i) ws[x[i]=r[i]]++;
    for(i=1;i<m;++i) ws[i]+=ws[i-1];
    for(i=n-1;i>=0;--i) sa[--ws[x[i]]]=i;
    for(j=1,p=1;p<n;j<<=1,m=p){
        for(p=0,i=n-j;i<n;++i) y[p++]=i;
        for(i=0;i<n;++i) if(sa[i]>=j) y[p++]=sa[i]-j;
        for(i=0;i<n;++i) wv[i]=x[y[i]];
        for(i=0;i<m;++i) ws[i]=0;
        for(i=0;i<n;++i) ws[wv[i]]++;
        for(i=1;i<m;++i) ws[i]+=ws[i-1];
        for(i=n-1;i>=0;--i) sa[--ws[wv[i]]]=y[i];
        for(t=x,x=y,y=t,p=1,x[sa[0]]=0,i=1;i<n;++i)
            x[sa[i]]=cmp(y,sa[i-1],sa[i],j)?p-1:p++;
    }
}

void calheight(int *r,int *sa,int n){
    int i,j,k=0;
    for(i=1;i<=n;++i) rank[sa[i]]=i;
    for(i=0;i<n;height[rank[i++]]=k)
        for(k?k--:0,j=sa[rank[i]-1];r[i+k]==r[j+k];k++);
}



引用:国家集训队论文《后缀数组——处理字符串的有力工具》罗穗骞

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值