后缀数组详解

后缀数组是一种处理字符串的算法,一般用倍增实现。

基本定义

后缀是指从某个位置 i i i开始到整个串末尾结束的一个特殊子串。字符串 r r r的第 i i i个字符开始的后缀的表示为 s u f f i x ( i ) suffix(i) suffix(i),也就是 s u f f i x ( i ) = r [ i … l e n ( r ) − 1 ] suffix(i)=r[i\dots len(r)-1] suffix(i)=r[ilen(r)1]

后缀数组 s a sa sa是一个一维数组,它保存 1 … n 1\dots n 1n的某个排列 s a [ 1 ] , s a [ 2 ] , … , s a [ n ] sa[1],sa[2],\dots,sa[n] sa[1],sa[2],,sa[n],并且保证 s u f f i x ( s a [ i ] ) < s u f f i x ( s a [ i + 1 ] ) , 1 ≤ i < n suffix(sa[i])<suffix(sa[i+1]),1\leq i<n suffix(sa[i])<suffix(sa[i+1]),1i<n。也就是将 S S S n n n个后缀从小到大排序后把排好序的后缀的开头位置一次放入 s a sa sa中。

名次数组 r a n k [ i ] rank[i] rank[i]保存的是 s u f f i x ( i ) suffix(i) suffix(i)在所有后缀中从小到大排列的名次。

举个例子:
在这里插入图片描述
在求出名次数组后,可以仅用 O ( 1 ) O(1) O(1)的时间比较任意两个字符的大小

倍增算法

如何求出这两个数组呢?一般使用的是倍增算法。用倍增的方法对每个字符开始的长度为 2 k 2^k 2k的子串进行排序,求出 r a n k rank rank数组。k从0开始,每次加1,当 2 k 2^k 2k大于 n n n以后,每个字符开始的长度为 2 k 2^k 2k的子串就相当于每个后缀。并且因为没有两个相同的字符串,所以 r a n k rank rank中没有相同的值。
在这里插入图片描述

code

int n,m,r[maxn],wa[maxn],wb[maxn],wv[maxn],vs[maxn],sa[maxn];
bool cmp(int *rr,int a,int b,int l){
    return rr[a]==rr[b]&&rr[a+l]==rr[b+l];
}
void dd(){
    int i,j,p,*x=wa,*y=wb,*t;
    memset(vs,0,sizeof(vs));
    for(i=0;i<n;i++) ++vs[x[i]=r[i]];
    for(i=1;i<m;i++) vs[i]+=vs[i-1];
    for(i=n-1;i>=0;i--) sa[--vs[x[i]]]=i;
    for(j=1,p=1;p<n;j*=2,m=p){
        for(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]];
        memset(vs,0,sizeof(vs));
        for(i=0;i<n;i++) ++vs[wv[i]];
        for(i=1;i<m;i++) vs[i]+=vs[i-1];
        for(i=n-1;i>=0;i--) sa[--vs[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++;
        }
    }
}

待排序的字符串转换为int类型存储在r数组中,从 r [ 0 ] r[0] r[0] r [ n − 1 ] r[n-1] r[n1],长度为n,且范围在0和m之间。为了操作方便,规定除 r [ n − 1 ] r[n-1] r[n1]外所有 r [ i ] r[i] r[i]都大于0, r [ n − 1 ] = 0 r[n-1]=0 r[n1]=0 s a sa sa数组从 s a [ 0 ] sa[0] sa[0] s a [ n − 1 ] sa[n-1] sa[n1]

最长公共前缀

h e i g h t [ i ] height[i] height[i] s u f f i x ( s a [ i − 1 ] ) suffix(sa[i-1]) suffix(sa[i1]) s u f f i x ( s a [ i ] ) suffix(sa[i]) suffix(sa[i])的最长公共前缀,也就是排名相邻的两个后缀的最长公共前缀。对于j和k,不妨设 r a n k [ j ] < r a n k [ k ] rank[j]<rank[k] rank[j]<rank[k],则有以下性质:

s u f f i x ( j ) suffix(j) suffix(j) s u f f i x ( k ) suffix(k) suffix(k)的最长公共前缀为 h e i g h t [ r a n k [ j ] + 1 ] , h e i g h t [ r a n k [ j ] + 2 ] , … h e i g h t [ r a n k [ k ] ] height[rank[j]+1],height[rank[j]+2],\dots height[rank[k]] height[rank[j]+1],height[rank[j]+2],height[rank[k]]的最小值

比如字符串 a a b a a a a b aabaaaab aabaaaab,求后缀 a b a a a a b abaaaab abaaaab和后缀 a a a b aaab aaab的最长公共前缀,如图所示:

在这里插入图片描述
但如何求height的值呢?

如果从1到n-1计算,最坏情况下要 O ( n 2 ) O(n^2) O(n2)。我们可以定义 h [ i ] = h e i g h t [ r a n k [ i ] ] h[i]=height[rank[i]] h[i]=height[rank[i]],也就是 s u f f i x ( i ) suffix(i) suffix(i)和在它前一名的后缀的最长公共前缀。

h数组满足 h [ i ] ≥ h [ i − 1 ] − 1 h[i]\geq h[i-1]-1 h[i]h[i1]1

证明:

s u f f i x ( k ) suffix(k) suffix(k)是排在 s u f f i x ( i − 1 ) suffix(i-1) suffix(i1)前一名的后缀,则它们的最长公共前缀为 h [ i − 1 ] h[i-1] h[i1]。那么 s u f f i x ( k + 1 ) suffix(k+1) suffix(k+1)将排在 s u f f i x ( i ) suffix(i) suffix(i)前面(这里要求 h [ i − 1 ] > 1 h[i-1]>1 h[i1]>1,如果 h [ i − 1 ] ≤ 1 h[i-1]\leq 1 h[i1]1,原式显然成立)并且 s u f f i x ( k + 1 ) suffix(k+1) suffix(k+1) s u f f i x ( i ) suffix(i) suffix(i)的最长公共前缀是 h [ i − 1 ] − 1 h[i-1]-1 h[i1]1,所以 s u f f i x ( i ) suffix(i) suffix(i)和它前一名的后缀的最长公共前缀至少为 h [ i − 1 ] − 1 h[i-1]-1 h[i1]1。按照h从1到n的顺序计算,并利用h数组的性质,时间复杂度可降为 O ( n ) O(n) O(n)

实现的时候没有必要保存h数组,只需按照 h [ 1 ] , h [ 2 ] , … h [ n − 1 ] h[1],h[2],\dots h[n-1] h[1],h[2],h[n1] 的顺序即可

code

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

代码中的h数组指 h e i g h t height height数组

最长公共子串

公共子串:如果字符串L同时出现在字符串A和字符串B中,则称字符串L是字符串A和字符串B的公共子串

字符串的任何一个子串都是其某个后缀的前缀,求两个字符串的最长公共子串就是求两个字符串的后缀的最长公共前缀。如果枚举两个字符串的后缀,则时间复杂度为 O ( n 2 ) O(n^2) O(n2)。我们可以将一个字符串并在另一个字符串后面,中间用一个没有出现过的字符隔开,再求新字符串的后缀数组。

A = a a a b a , B = a b a a A=aaaba,B=abaa A=aaaba,B=abaa 为例,如图所示:
在这里插入图片描述
因为最长公共子串为A和B的后缀的最长公共前缀,所以最长公共子串的长度就是满足条件的 h e i g h t height height值中的最大值。可取的 h e i g h t height height要满足什么条件呢?如果两个后缀在原来的同一个字符串,则不能满足条件。所以只有当 s u f f i x ( s a [ i − 1 ] ) suffix(sa[i-1]) suffix(sa[i1]) s u f f i x ( s a [ i ] ) suffix(sa[i]) suffix(sa[i])不是同一个字符串的两个后缀时, h e i g h t height height值才是满足条件的。记 A A A B B B的长度为 ∣ A ∣ |A| A ∣ B ∣ |B| B,则时间复杂度为 O ( ∣ A ∣ + ∣ B ∣ ) O(|A|+|B|) O(A+B)

  • 7
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
KMP算法是一种字符串匹配算法,用于在一个文本串S内查找一个模式串P的出现位置。其中,KMP算法的关键在于求解模式串P的next数组。下面详细介绍next数组的含义、求解方法以及应用。 1. 含义 next数组是一个长度为m(m为模式串P的长度)的数组,其中next[i]表示P[0:i]这个子串中,最长的既是其前缀又是其后缀的字符串的长度。特别地,next[0]=-1,next[1]=0。例如,当P="abab"时,其next数组为[-1,0,0,1]。 2. 求解 next数组的求解可以通过动态规划的方式实现。具体来说,在求解next[i]时,假设已知next[0:i-1]的值,我们需要找到一个最长的既是P[0:i-1]的前缀,也是P[1:i]的后缀的字符串。这个字符串可以通过比较P[0:j-1]和P[i-j:i-1]来得到,其中j=next[i-1]+1。 如果P[j]==P[i],那么next[i]=j;否则,我们需要找到一个更短的字符串。此时,我们可以利用next数组的性质,从next[j]开始向前查找,直到找到一个P[k]等于P[i]为止,然后令next[i]=k。如果一直找到k=-1还没有找到,那么next[i]=0。 3. 应用 有了next数组之后,我们就可以利用KMP算法在文本串S中查找模式串P的出现位置。具体来说,我们维护两个指针i和j,分别指向S和P的当前位置。如果P[j]==S[i],那么i和j都向后移动一位;否则,我们利用next数组来决定j的下一步移动位置。具体来说,如果next[j]=-1,或者next[j]<i,则令j=0,i不变;否则,令j=next[j]。这样,我们可以在O(n+m)的时间复杂度内完成匹配。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值