后缀数组-

后缀数组


代码

/*
n:代表字符串长度 m:代表字符集大小 
s数组:字符串数组,内容从下标1开始
rk数组:排名数组
c数组:基数排序的数组,下标为待排序的数字,值为该数字出现的次数。排序过程中,我们会对其求前缀和以便计算排名
x数组:是一个中间量数组,意义为得到第一关键字的大小,对于一次排序,下标为代表后缀编号,值为象征对应后缀编号第一关键字大小的值(事实上可以视作排名)
y数组:是一个中间量数组,意义为第二关键字的排名,对于一次排序,下标为排名,值为第二关键字对应的后缀的编号
*/
void get_sa()
{
    //统计该次第一关键字(首字母)的出现次数
    for (int i = 1; i <= n; i ++ ) c[x[i] = s[i]] ++ ;
    //计算前缀和
    for (int i = 2; i <= m; i ++ ) c[i] += c[i - 1];	
    //计算第一次排序的sa,此次基数排序,仅区分首字母和后缀编号,首字母小的在前,首字母相同,后缀编号小(后缀长度大)的在前
    for (int i = n; i; i -- ) sa[c[x[i]] -- ] = i;
    
    for (int k = 1; k <= n; k <<= 1)
    {
        //从21~31都是倍增计算y数组 先进行第二关键字排序
        int num = 0;
        //有一些后缀拆分后的第二关键字为空,这个循环将这些后缀的排名提到最前,以表示空字符的最高优先级
        for (int i = n - k + 1; i <= n; i ++ ) y[ ++ num] = i;
        /*
        对于满的第二关键字排序,利用某一后缀的后缀也是原串的后缀这一特点,由上一次的sa计算而来.
        考虑那些排名靠前的后缀,如果往前补倍增长度位,那么这个后缀对应的长度前缀就变成了第二关键字
        比如我们有后缀aa和abaa,长度为2,计算abaa的第二关键字的时候,实际上查到的是aa的排名,并且往前补了2位。
        */
        for (int i = 1; i <= n; i ++ )
            if (sa[i] > k)
                y[ ++ num] = sa[i] - k;
        
        /*
        从36~44是基数排序倍增计算x[] 这是对第一关键字排序 此时求得的x[]含义是旧的排名,是长度为2^k的排名
        */
        for (int i = 1; i <= m; i ++ ) c[i] = 0;	//清空
        //y[i]表示的是第二关键字排名为i的后缀编号  
        //而x[i]拿到的则是第一关键字排序时,后缀编号为i的后缀对应的象征第一关键字大小的值
        //(在初始化过程中,我们将该值设置为首字母的值了,因此,对初始化还有一种理解,即y[i] = i,并没对第二关键字排序)
        //因此x[y[i]]的值反映的是代表第一关键字大小的值
        for (int i = 1; i <= n; i ++ ) c[x[y[i]]] ++ ;
        for (int i = 2; i <= m; i ++ ) c[i] += c[i - 1];
        //对i逆序统计,其用意是:当第一关键字相同的时候,第二关键字排名大的,计算后sa的值也会更大
        for (int i = n; i; i -- ) sa[c[x[y[i]]] -- ] = y[i], y[i] = 0;
        
        //从48~54都是在求新的x[],即求长度为2^(k+1)的排名
        //y已经清空了,把旧的x数组值换到y数组上
        swap(x, y);
        //规定当前排名第一的后缀x值为1,以后的后缀x值不会小于1
        x[sa[1]] = 1, num = 1;
        //考虑什么时候一个后缀和前面的后缀下一次的第一关键字=这一次的第一+第二关键字,因为是倍增)相同
        //只有这一次的第一第二关键字都相等才可以判定下一次的第一关键字相等,否则应该比上一次的大。
        for (int i = 2; i <= n; i ++ )
            x[sa[i]] = (y[sa[i]] == y[sa[i - 1]] && y[sa[i] + k] == y[sa[i - 1] + k]) ? num : ++ num;
        //当本次排序中已经产生了不小于n种不同的第一关键字的值,排序便没有必要继续下去了,因为已经区分开了这些后缀。
        if (num == n) break;
        //本次排序更新了关键字的种类数,因此要更新m值  更新基数排序的值域
        m = num;
    }
}

后缀数组的相关概念

后缀

后缀:指从某个位置开始到字符串末尾的一个特殊子串。字符串 s s s从第 i i i个字符开始的后缀被表示为 S u f f i x ( i ) Suffix(i) Suffix(i),也可以称之为下标为 i i i的后缀。

例如字符串 s = s= s=" a a b a a a a b aabaaaab aabaaaab",其所有的后缀如下:

  • S u f f i x ( 0 ) = Suffix(0)= Suffix(0)= a a b a a a a b aabaaaab aabaaaab
  • S u f f i x ( 1 ) = Suffix(1)= Suffix(1)= a b a a a a b abaaaab abaaaab
  • S u f f i x ( 2 ) = Suffix(2)= Suffix(2)= b a a a a b baaaab baaaab
  • S u f f i x ( 3 ) = Suffix(3)= Suffix(3)= a a a a b aaaab aaaab
  • S u f f i x ( 4 ) = Suffix(4)= Suffix(4)= a a a b aaab aaab
  • S u f f i x ( 5 ) = Suffix(5)= Suffix(5)= a a b aab aab
  • S u f f i x ( 6 ) = Suffix(6)= Suffix(6)= a b ab ab
  • S u f f i x ( 7 ) = Suffix(7)= Suffix(7)= b b b
后缀数组

将所有后缀都从小到大排序之后,将排好序的后缀的下标 i i i放入数组中,则该数组就叫做后缀数组(Suffix Array), S A [ n u m ] = i SA[num]=i SA[num]=i表示排名为 n u m num num的后缀其下标为 i i i

将上面的所有后缀都按字典序排序之后,取其下标 i i i,即可得到后缀数组:

  • S u f f i x ( 3 ) = Suffix(3)= Suffix(3)= a a a a b aaaab aaaab
  • S u f f i x ( 4 ) = Suffix(4)= Suffix(4)= a a a b aaab aaab
  • S u f f i x ( 5 ) = Suffix(5)= Suffix(5)= a a b aab aab
  • S u f f i x ( 0 ) = Suffix(0)= Suffix(0)= a a b a a a a b aabaaaab aabaaaab
  • S u f f i x ( 6 ) = Suffix(6)= Suffix(6)= a b ab ab
  • S u f f i x ( 1 ) = Suffix(1)= Suffix(1)= a b a a a a b abaaaab abaaaab
  • S u f f i x ( 7 ) = Suffix(7)= Suffix(7)= b b b
  • S u f f i x ( 2 ) = Suffix(2)= Suffix(2)= b a a a a b baaaab baaaab

后缀数组SA[]={3,4,5,0,6,1,7,2}

排名数组

排名数组是指下标为 i i i的后缀排序后的名次,即第 i i i个后缀排名是啥, r a n k [ i ] = n u m rank[i]=num rank[i]=num表示第 i i i个后缀其排名为 n u m num num

名次下标后缀
numiSuffix(i)
13aaaab
24aaab
35aab
40aabaaaab
56ab
61abaaaab
77b
82baaaab

下标为3的后缀,排名为1,即 r a n k [ 3 ] = 1 rank[3]=1 rank[3]=1;排名第1的后缀,下标为 3 3 3,即 S A [ 1 ] = 3 SA[1]=3 SA[1]=3。排名数组和后缀数组是互逆的,可以相互转换,并且 r a n k [ S A [ l ] ] = l rank[SA[l]]=l rank[SA[l]]=l S A [ r a n k [ i ] ] = i SA[rank[i]]=i SA[rank[i]]=i

r k [ ] rk[] rk[]推导 s a [ ] sa[] sa[]

for(int i=0;i<n;i++)
    sa[rk[i]]=i;

s a [ ] sa[] sa[]推导 r k [ ] rk[] rk[]

for(int i=0;i<n;i++)
    rk[sa[i]]=i;

后缀数组的构建思路

构建后缀数组有两种方法: D C 3 DC3 DC3算法和倍增算法。前者时间复杂度为 O ( n ) O(n) O(n),后者时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)。一般来说都是使用倍增算法

采用倍增算法,对字符串从每个下标开始的长度为 2 k 2^k 2k的子串进行排序,得到排名。 k k k从0开始,每次都增加1,相当于长度增加了1倍。当 2 k ≥ n 2^k\geq n 2kn时,从每个下标开始的长度为 2 k 2^k 2k的子串都相当于所有后缀。每次子串排序都需要用到上一次子串的排名得到。

图解如下:

设字符串 s s s=" a a b a a a a b aabaaaab aabaaaab"

(1)将字符串 s s s从每个下标开始长度为1的子串进行排名,一般来说都是将该字符的ASCII码作为其排名,但是这里直接将每个字符转成数字 s [ i ] − s[i]- s[i] a a a’+ 1 1 1,如下图:

image-20210811213313872

(2)求解长度为 2 1 2^1 21的子串排名,将上一次 r a n k rank rank值的第 i i i个和第 i + 1 i+1 i+1个结合,相当于得到长度为 2 1 2^1 21的子串的每个位置排名,然后排序,即可得到长度为 2 1 2^1 21的子串排名,如下图:

image-20210811213614247

(3)求解长度为 2 2 2^2 22的子串排名,将上一次 r a n k rank rank值的第 i i i个和第 i + 2 i+2 i+2个结合,相当于得到长度为 2 2 2^2 22的子串的每个位置排名,然后排序,即可得到长度为 2 2 2^2 22的子串排名,如下图:

image-20210811213636706

(4)求解长度为 2 3 2^3 23的子串排名,将上一次 r a n k rank rank值的第 i i i个和第 i + 4 i+4 i+4个结合,相当于得到长度为 2 3 2^3 23的子串的每个位置排名,然后排序,即可得到长度为 2 3 2^3 23的子串排名,如下图:

image-20210811213748859

由于此时 2 k = 2 3 = 8 2^k=2^3=8 2k=23=8,已经等于了 n = 8 n=8 n=8,因此,结束。

我们发现第4步和第3步的结果是相同的,实际上,如果在 r a n k rank rank没有相同值时已经得到了后缀排名,就不需要再继续运算了。这主要是根据字符串比较的规则,如果两个字符串的前面几个字符已经比出了大小关系,那么就不再需要考虑后面字符的比较了。

将排名数组转换成后缀数组,排名第1的下标为3,排名第2的下标为4,排名第3的下标为5,排名第4的下标为0,排名第5的下标为6,排名第6的下标为1,排名第7的下标为7,排名第8的下标为2

因此得到后缀数组SA[]={3,4,5,0,6,1,7,2}

因为是倍增算法,每次比较的字符数都翻倍,因此长度为 n n n的字符串最多需要 O ( l o g n ) O(logn) O(logn)次排序,采用基数排序,其时间复杂度为 O ( n ) O(n) O(n),因此进行 n n n趟排序后,求解后缀数组总的时间复杂度就是 O ( n l o g n ) O(nlogn) O(nlogn)

sub[i][k]表示 s s s i i i开始长度为 2 k 2^k 2k的子串, s u b [ i ] [ k ] sub[i][k] sub[i][k]其实也就是 s [ i ] , s [ i + 1 ] , ⋯   , s [ i + ( 1 < < k ) − 1 ] s[i],s[i+1],\cdots,s[i+(1<<k)-1] s[i],s[i+1],,s[i+(1<<k)1],超过 n n n的部分都被视为’\0’(ASCII为0,字典序最小的字符,即空字符)

rank[i][k]表示 s u b [ i ] [ k ] sub[i][k] sub[i][k]在长度为 2 k 2^k 2k的所有子串中的排名

sa[l][k]表示在长度为 2 k 2^k 2k的所有子串中排名第 l l l的子串的开始位置

倍增过程:

  • step 1:求出长度为 2 0 2^0 20的子串的字典排序,即求出 s u b [ 1 ] [ 0 ] sub[1][0] sub[1][0] s u b [ 2 ] [ 0 ] sub[2][0] sub[2][0] ⋯ \cdots s u b [ n ] [ 0 ] sub[n][0] sub[n][0]的字典排序

  • step 2:求出长度为 2 1 2^1 21的子串的字典排序,即求出 s u b [ 1 ] [ 1 ] sub[1][1] sub[1][1] s u b [ 2 ] [ 1 ] sub[2][1] sub[2][1] ⋯ \cdots s u b [ n ] [ 1 ] sub[n][1] sub[n][1]的字典排序

  • ⋯ \cdots

  • step k:求出长度为 2 k 2^k 2k的子串的字典排序,即求出 s u b [ 1 ] [ k ] sub[1][k] sub[1][k] s u b [ 2 ] [ k ] sub[2][k] sub[2][k] ⋯ \cdots s u b [ n ] [ k ] sub[n][k] sub[n][k]的字典排序

  • 当子串长度 2 k ≥ n 2^k\geq n 2kn时,子串排序就是后缀排序

现在来思考一下,如何利用 r a n k [ 1 ⋯ n ] [ k ] rank[1\cdots n][k] rank[1n][k],求出 r a n k [ 1 ⋯ n ] [ k + 1 ] rank[1\cdots n][k+1] rank[1n][k+1]呢?

对于两个子串 s u b [ i ] [ k + 1 ] sub[i][k+1] sub[i][k+1] s u b [ j ] [ k + 1 ] sub[j][k+1] sub[j][k+1],我们可以这么比较:将长度一分为二,先比较前一半,如果前一半能得到结果则结束;否则再接着比较后一半

  • 先比较 r a n k [ i ] [ k ] rank[i][k] rank[i][k] r a n k [ j ] [ k ] rank[j][k] rank[j][k]
  • 如果相等,再比较 r a n k [ i + 2 k ] rank[i+2^k] rank[i+2k] r a n k [ j + 2 k ] rank[j+2^k] rank[j+2k]
  • 那么我们发现,其实我们就是再对二元组 ( r a n k [ i ] [ k ] , r a n k [ i + 2 k ] [ k ] ) (rank[i][k],rank[i+2^k][k]) (rank[i][k],rank[i+2k][k])进行排序。联想C++中pair二元组可知,先按first比较,如果相等,再按second比较。受此启发,我们可以知道要解决这个问题,需要对第一个关键字和第二个关键字进行排序

从上可以观察到一个性质:想要求出长度为 k + 1 k+1 k+1的排名,需要用到长度为 k k k的排名

如下图所示:

image-20210811221227880

注意到 r a n k [ i ] [ j ] rank[i][j] rank[i][j]值域是不会超过 n n n的正整数(可能会有相同),因此可以使用基数排序。对于一个三位的十进制数,如果使用基数排序,则先排个位,再排十位,最后排百位,即从低位到高位进行排序。联想C++中的pair是 < f i r s t , s e c o n d > <first,second> <first,second>,那么我们可以类比于十进制的基数排序,从右到左分别是从低位到高位,因此对于二元组排序来说,是先排second,再排first,即先排第二关键字,然后再排第一关键字。


后缀数组的实现

如何理解下面这段代码呢?

这一段代码其实是处理长度为 2 k = 2 0 = 1 2^k=2^0=1 2k=20=1的单个字符的后缀

这里 n n n是该字符串 s s s的长度, m = 122 m=122 m=122

    for (int i = 1; i <= n; i ++ ) c[x[i] = s[i]] ++ ;
    for (int i = 2; i <= m; i ++ ) c[i] += c[i - 1];
    for (int i = n; i; i -- ) sa[c[x[i]] -- ] = i;

首先来理解第一行代码:

for (int i = 1; i <= n; i ++ ) c[x[i] = s[i]] ++ ;

x[i]=s[i]表示将 s [ i ] s[i] s[i]这个字符的ASCII值作为这个字符的排名,假设字符串 s s s a d c a b adcab adcab,那么有:

  • x [ 1 ] = s [ 1 ] = x[1]=s[1]= x[1]=s[1]= a a a’=97, c [ 97 ] c[97] c[97]++,所以 c [ 97 ] = 1 c[97]=1 c[97]=1
  • x [ 2 ] = s [ 2 ] = x[2]=s[2]= x[2]=s[2]= d d d’=100, c [ 100 ] c[100] c[100]++,所以 c [ 100 ] = 1 c[100]=1 c[100]=1
  • x [ 3 ] = s [ 3 ] = x[3]=s[3]= x[3]=s[3]= c c c’=99, c [ 99 ] c[99] c[99]++,所以 c [ 99 ] = 1 c[99]=1 c[99]=1
  • x [ 4 ] = s [ 4 ] = x[4]=s[4]= x[4]=s[4]= a a a’=97, c [ 97 ] c[97] c[97]++,所以 c [ 97 ] = 2 c[97]=2 c[97]=2
  • x [ 5 ] = s [ 5 ] = x[5]=s[5]= x[5]=s[5]= b b b’=98, c [ 98 ] c[98] c[98]++,所以 c [ 98 ] = 1 c[98]=1 c[98]=1

再来理解第二行代码:

for (int i = 2; i <= m; i ++ ) c[i] += c[i - 1];

这里其实就是类似于求前缀和

  • i = 97 i=97 i=97时, c [ 97 ] = c [ i ] + c [ i − 1 ] = c [ 97 ] + c [ 96 ] = 2 + 0 = 2 c[97]=c[i]+c[i-1]=c[97]+c[96]=2+0=2 c[97]=c[i]+c[i1]=c[97]+c[96]=2+0=2
  • i = 98 i=98 i=98时, c [ 98 ] = c [ i ] + c [ i − 1 ] = c [ 98 ] + c [ 97 ] = 1 + 2 = 3 c[98]=c[i]+c[i-1]=c[98]+c[97]=1+2=3 c[98]=c[i]+c[i1]=c[98]+c[97]=1+2=3
  • i = 99 i=99 i=99时, c [ 99 ] = c [ i ] + c [ i − 1 ] = c [ 99 ] + c [ 98 ] = 1 + 3 = 4 c[99]=c[i]+c[i-1]=c[99]+c[98]=1+3=4 c[99]=c[i]+c[i1]=c[99]+c[98]=1+3=4
  • i = 100 i=100 i=100时, c [ 100 ] = c [ i ] + c [ i − 1 ] = c [ 100 ] + c [ 99 ] = 1 + 4 = 5 c[100]=c[i]+c[i-1]=c[100]+c[99]=1+4=5 c[100]=c[i]+c[i1]=c[100]+c[99]=1+4=5

最后来理解第三行代码:

for (int i = n; i; i -- ) sa[c[x[i]] -- ] = i;
  • i = 5 i=5 i=5时,$x[5]=9 8 , 8, 8c[98]=3 , 则 ,则 sa[3]=5 , 然 后 ,然后 c[98] − − , 于 是 --,于是 c[98]=2$
  • i = 4 i=4 i=4时, x [ 4 ] = 97 x[4]=97 x[4]=97 c [ 97 ] = 2 c[97]=2 c[97]=2,则 s a [ 2 ] = 4 sa[2]=4 sa[2]=4,然后 c [ 97 ] c[97] c[97]–,于是 c [ 97 ] = 1 c[97]=1 c[97]=1
  • i = 3 i=3 i=3时, x [ 3 ] = 99 x[3]=99 x[3]=99 c [ 99 ] = 4 c[99]=4 c[99]=4,则 s a [ 4 ] = 3 sa[4]=3 sa[4]=3,然后 c [ 99 ] c[99] c[99]–,于是 c [ 99 ] = 3 c[99]=3 c[99]=3
  • i = 2 i=2 i=2时, x [ 2 ] = 100 x[2]=100 x[2]=100 c [ 100 ] = 5 c[100]=5 c[100]=5,则 s a [ 5 ] = 2 sa[5]=2 sa[5]=2,然后 c [ 100 ] c[100] c[100]–,于是 c [ 100 ] = 4 c[100]=4 c[100]=4
  • i = 1 i=1 i=1时, x [ 1 ] = 97 x[1]=97 x[1]=97 c [ 97 ] = 1 c[97]=1 c[97]=1,则 s a [ 1 ] = 1 sa[1]=1 sa[1]=1,然后 c [ 97 ] c[97] c[97]–,于是 c [ 97 ] = 0 c[97]=0 c[97]=0
  • 因此得到长度为 2 k = 2 0 = 1 2^k=2^0=1 2k=20=1的子串的后缀数组SA[]={1,4,5,3,2}

图示理解如下:

image-20210812011156027

总结:

for k=1~logn

  • 先按第二关键字排序,即按 r a n k [ i + 2 k ] [ k ] rank[i+2^k][k] rank[i+2k][k]进行基数排序
  • 然后再按第一关键字排序,即按 r a n k [ i ] [ k ] rank[i][k] rank[i][k]进行基数排序
  • 通过上面图片中的栗子可以看出,求出了 r a n g k [ i + 2 k ] [ k ] rangk[i+2^k][k] rangk[i+2k][k] r a n k [ i ] [ k ] rank[i][k] rank[i][k]后,就可以递推得到了 k + 1 k+1 k+1时的后缀数组Sa[],即得到 s a [ i ] [ k + 1 ] sa[i][k+1] sa[i][k+1]
  • 最后我们又用 s a [ i ] [ k + 1 ] sa[i][k+1] sa[i][k+1]求出了 k + 1 k+1 k+1时的排名数组 r a n k [ i ] [ k + 1 ] rank[i][k+1] rank[i][k+1]

如何理解下面这段代码:

        for (int i = n - k + 1; i <= n; i ++ ) y[ ++ num] = i;
        for (int i = 1; i <= n; i ++ )
            if (sa[i] > k)
                y[ ++ num] = sa[i] - k;

理解如下:

比如字符串 s s s a a b a a a a b aabaaaab aabaaaab,我们将每个字符都转换成数字,转换规则是 s [ i ] − ′ a ′ + 1 s[i]-'a'+1 s[i]a+1,求出长度为 k = 0 , 2 k = 2 0 = 1 k=0,2^k=2^0=1 k=0,2k=20=1的子串的排名数组 x [ ] x[] x[](相当于rank[]),进行基数排序,为了放置比较时越界,在末尾用0封装:

image-20210812125304993

接下来求解子串长度为 k = 1 , 2 k = 2 1 = 2 k=1,2^k=2^1=2 k=1,2k=21=2的:

我们对第二关键字进行基数排序,将排序结果存储在y[]中:

image-20210812132933201

image-20210812132432956

从上面我们发现,将上一次的排名x[]前移错位(-k),就可以得到第2关键字的排序结果(下标)y[]

如何理解下面这段代码:

        for (int i = 1; i <= m; i ++ ) c[i] = 0;
        for (int i = 1; i <= n; i ++ ) c[x[y[i]]] ++ ;
        for (int i = 2; i <= m; i ++ ) c[i] += c[i - 1];
        for (int i = n; i; i -- ) sa[c[x[y[i]]] -- ] = y[i], y[i] = 0;

理解如下:

这段代码其实是在对第1关键字进行基数排序,特别是需要注意理解c[x[y[i]]] ++sa[c[x[y[i]]] -- ] = y[i]是什么意思:

image-20210812133551552

  • i = 0 i=0 i=0时, y [ 0 ] = 8 y[0]=8 y[0]=8,然后 x [ 8 ] = 0 x[8]=0 x[8]=0,而这个0刚好就是补0的空字符的第1关键字的大小
  • i = 1 i=1 i=1时, y [ 1 ] = 7 y[1]=7 y[1]=7,然后 x [ 7 ] = 2 x[7]=2 x[7]=2,而这个2刚好就是第1关键字’b’的大小
  • i = 2 i=2 i=2时, y [ 2 ] = 0 y[2]=0 y[2]=0,然后 x [ 0 ] = 1 x[0]=1 x[0]=1,而这个1刚好就是第1关键字’a’的大小
  • i = 3 i=3 i=3时,$y[3]= 2 , 然 后 2,然后 2x[2]=2$,而这个2刚好就是第1关键字’b’的大小
  • i = 4 i=4 i=4时, y [ 4 ] = 3 y[4]=3 y[4]=3,然后 x [ 3 ] = 1 x[3]=1 x[3]=1,而这个1刚好就是第1关键字’a’的大小

因此,我们发现:将第2关键字的排序结果y[]转换成排名,正好是第1关键字!由于我们是对第1关键字进行基数排序,因此通过x[y[i]]的转换,就巧妙地利用已经求出来的y[]来得到我们想要的第1关键字x[]

因此这就解释了c[x[y[i]]] ++

对第1关键字进行基数排序,按第1关键字的排名顺序将x[]中的值 x [ i ] x[i] x[i]作为桶的编号,由上图可知, x [ ] x[] x[]的值域有0、1、2,然后将 y [ i ] y[i] y[i]的值丢入桶中:

  • i = 0 i=0 i=0时, y [ 0 ] = 8 y[0]=8 y[0]=8,这个8丢入桶中, x [ 8 ] = 0 x[8]=0 x[8]=0,即 x [ y [ 0 ] ] = 0 x[y[0]]=0 x[y[0]]=0,这个0作为桶的编号
  • i = 1 i=1 i=1时, y [ 1 ] = 7 y[1]=7 y[1]=7,这个7丢入桶中, x [ 7 ] = 2 x[7]=2 x[7]=2,即 x [ y [ 1 ] ] = 2 x[y[1]]=2 x[y[1]]=2,这个2作为桶的编号
  • i = 2 i=2 i=2时, y [ 2 ] = 0 y[2]=0 y[2]=0,这个0丢入桶中, x [ 0 ] = 1 x[0]=1 x[0]=1,即 x [ y [ 2 ] ] = 1 x[y[2]]=1 x[y[2]]=1,这个1作为桶的编号
  • i = 3 i=3 i=3时, y [ 3 ] = 2 y[3]=2 y[3]=2,这个2丢入桶中, x [ 2 ] = 2 x[2]=2 x[2]=2,即 x [ y [ 3 ] ] = 2 x[y[3]]=2 x[y[3]]=2,这个2作为桶的编号
  • i = 4 i=4 i=4时, y [ 4 ] = 3 y[4]=3 y[4]=3,这个3丢入桶中, x [ 3 ] = 1 x[3]=1 x[3]=1,即 x [ y [ 4 ] ] = 1 x[y[4]]=1 x[y[4]]=1,这个1作为桶的编号
  • i = 5 i=5 i=5时, y [ 5 ] = 4 y[5]=4 y[5]=4,这4丢入桶中, x [ 4 ] = 1 x[4]=1 x[4]=1,即 x [ y [ 5 ] ] = 1 x[y[5]]=1 x[y[5]]=1,这个1作为桶的编号
  • i = 6 i=6 i=6时, y [ 6 ] = 5 y[6]=5 y[6]=5,这个5丢入桶中, x [ 5 ] = 1 x[5]=1 x[5]=1,即 x [ y [ 6 ] ] = 1 x[y[6]]=1 x[y[6]]=1,这个1作为桶的编号
  • i = 7 i=7 i=7时, y [ 7 ] = 1 y[7]=1 y[7]=1,这个1丢入桶中, x [ 1 ] = 1 x[1]=1 x[1]=1,即 x [ y [ 7 ] ] = 1 x[y[7]]=1 x[y[7]]=1,这个1作为桶的编号
  • i = 8 i=8 i=8时, y [ 8 ] = 6 y[8]=6 y[8]=6,这个6丢入桶中, x [ 6 ] = 1 x[6]=1 x[6]=1,即 x [ y [ 8 ] ] = 1 x[y[8]]=1 x[y[8]]=1,这个1作为桶的编号

如下图:

image-20210812164242433

因此这就是解释了sa[c[x[y[i]]] -- ] = y[i]

如何理解这段代码呢?

        swap(x, y);
        x[sa[1]] = 1, num = 1;
        for (int i = 2; i <= n; i ++ )
            x[sa[i]] = (y[sa[i]] == y[sa[i - 1]] && y[sa[i] + k] == y[sa[i - 1] + k]) ? num : ++ num;
        if (num == n) break;
        m = num;

理解如下:

根据 s a [ ] sa[] sa[] x [ ] x[] x[]数组计算新的排名数组(长度为2的子串排名)。因为要使用旧的x[]数组计算新的x[]数组,由于我们之前已经把 y [ i ] y[i] y[i]都清0了,因此此时的y[]是空的没有用了,因此将 x [ ] x[] x[] y [ ] y[] y[]交换,swap(x,y),此时的 y [ ] y[] y[]数组就是旧的 x [ ] x[] x[]数组了,然后现在需要计算新的 x [ ] x[] x[]数组

我们初始化x[sa[1]] = 1,因为 s a [ 1 ] sa[1] sa[1]表示排名为1的子串所对应的下标位置,既然这个子串它是排名第1,那么它在新的排名中也仍然是第一,因此我们把它初始化为1

下面举个栗子:

设字符串 s s s为 $aabaab

k = 0 k=0 k=0时,已经处理完了长度为 2 0 2^0 20的子串了

然后现在来处理 k = 1 k=1 k=1,即长度为 2 1 2^1 21的子串:

image-20210812112112094

接下来理解如何用得到的新的SA[]数组求出下一轮的排名数组tempRA[],即通过 s a [ i ] [ k + 1 ] sa[i][k+1] sa[i][k+1]求出 r a n k [ i ] [ k + 1 ] rank[i][k+1] rank[i][k+1]

我们从上往下扫描,即 i = 0 i=0 i=0扫描到 i = 8 i=8 i=8,比较当前 i i i这一层中的 R A [ S A [ i ] ] RA[SA[i]] RA[SA[i]] R A [ S A [ i ] + k ] RA[SA[i]+k] RA[SA[i]+k]与上一层 i − 1 i-1 i1中的 R A [ S A [ i ] ] RA[SA[i]] RA[SA[i]] R A [ S A [ i ] + k ] RA[SA[i]+k] RA[SA[i]+k],如果都相等,则说明它俩都是同一个排名,如果第 i i i层的大,则说明第 i i i层的排名比上一层 i − 1 i-1 i1的排名多1

  • i = 0 i=0 i=0时, t e m p R A [ 0 ] = 0 tempRA[0]=0 tempRA[0]=0
  • i = 1 i=1 i=1时,由于 R A [ S A [ 1 ] ] > R A [ S A [ 0 ] ] RA[SA[1]]>RA[SA[0]] RA[SA[1]]>RA[SA[0]],因此, t e m p R A [ 1 ] = 1 tempRA[1]=1 tempRA[1]=1
  • i = 2 i=2 i=2时,由于 R A [ S A [ 2 ] ] = R A [ S A [ 1 ] ] RA[SA[2]]=RA[SA[1]] RA[SA[2]]=RA[SA[1]]并且 R A [ S A [ 2 ] + k ] = R A [ S A [ 1 ] + k ] RA[SA[2]+k]=RA[SA[1]+k] RA[SA[2]+k]=RA[SA[1]+k],因此 t e m p R A [ 2 ] = t e m p R A [ 1 ] = 1 tempRA[2]=tempRA[1]=1 tempRA[2]=tempRA[1]=1
  • i = 3 i=3 i=3时,由于 R A [ S A [ 3 ] ] = R A [ S A [ 2 ] ] RA[SA[3]]=RA[SA[2]] RA[SA[3]]=RA[SA[2]]并且 R A [ S A [ 3 ] + k ] = R A [ S A [ 2 ] + k ] RA[SA[3]+k]=RA[SA[2]+k] RA[SA[3]+k]=RA[SA[2]+k],因此 t e m p R A [ 3 ] = t e m p R A [ 2 ] = 1 tempRA[3]=tempRA[2]=1 tempRA[3]=tempRA[2]=1
  • i = 4 i=4 i=4时,由于 R A [ S A [ 4 ] ] = R A [ S A [ 3 ] ] RA[SA[4]]=RA[SA[3]] RA[SA[4]]=RA[SA[3]]并且 R A [ S A [ 4 ] + k ] = R A [ S A [ 3 ] + k ] RA[SA[4]+k]=RA[SA[3]+k] RA[SA[4]+k]=RA[SA[3]+k],因此 t e m p R A [ 4 ] = t e m p R A [ 3 ] = 1 tempRA[4]=tempRA[3]=1 tempRA[4]=tempRA[3]=1
  • i = 5 i=5 i=5时,由于 R A [ S A [ 5 ] ] = R A [ S A [ 4 ] ] RA[SA[5]]=RA[SA[4]] RA[SA[5]]=RA[SA[4]]但是 R A [ S A [ 5 ] + k ] > R A [ S A [ 4 ] + k ] RA[SA[5]+k]>RA[SA[4]+k] RA[SA[5]+k]>RA[SA[4]+k],因此 t e m p R A [ 5 ] = 2 tempRA[5]=2 tempRA[5]=2
  • i = 6 i=6 i=6时,由于 R A [ S A [ 6 ] ] = R A [ S A [ 5 ] ] RA[SA[6]]=RA[SA[5]] RA[SA[6]]=RA[SA[5]]并且 R A [ S A [ 6 ] + k ] = R A [ S A [ 5 ] + k ] RA[SA[6]+k]=RA[SA[5]+k] RA[SA[6]+k]=RA[SA[5]+k],因此 t e m p R A [ 6 ] = 2 tempRA[6]=2 tempRA[6]=2
  • i = 7 i=7 i=7时,由于 R A [ S A [ 7 ] ] > R A [ S A [ 6 ] ] RA[SA[7]]>RA[SA[6]] RA[SA[7]]>RA[SA[6]],因此 t e m p R A [ 7 ] = 3 tempRA[7]=3 tempRA[7]=3
  • i = 8 i=8 i=8时,由于 R A [ S A [ 8 ] ] = R A [ S A [ 7 ] ] RA[SA[8]]=RA[SA[7]] RA[SA[8]]=RA[SA[7]]但是 R A [ S A [ 8 ] + k ] > R A [ S A [ 7 ] + k ] RA[SA[8]+k]>RA[SA[7]+k] RA[SA[8]+k]>RA[SA[7]+k],因此 t e m p R A [ 8 ] = 4 tempRA[8]=4 tempRA[8]=4

image-20210812115250875

然后再根据 R A [ S A [ i ] ] RA[SA[i]] RA[SA[i]] R A [ S A [ i ] + 2 ] RA[SA[i]+2] RA[SA[i]+2]求出新的SA[]

再从上往下遍历,更新 t e m p R A [ ] tempRA[] tempRA[]

image-20210812115937390

然后当 k = 3 k=3 k=3时,子串长度为 2 3 2^3 23,需要用到子串长度为 2 2 2^2 22,因此我们将当 k = 2 k=2 k=2时得到的 t e m p R A [ ] tempRA[] tempRA[]作为 k = 3 k=3 k=3时的第一关键字,比如当 k = 3 k=3 k=3时为$aabaab,则我们需要用到前面四个

$aab,则这已经在上一层k=2时求出来了,于是直接把上一层k=2的排名直接复制给k=3时的第一关键字就好了:

如下图:

image-20210812121029257

通过上面一顿操作,我们发现,如果当前第 i i i层的 y [ s a [ i ] ] y[sa[i]] y[sa[i]]等于上一层 i − 1 i-1 i1层的 y [ s a [ i − 1 ] ] y[sa[i - 1]] y[sa[i1]],并且当前第 i i i层的 y [ s a [ i ] + k ] y[sa[i] + k] y[sa[i]+k]等于上一层 i − 1 i-1 i1层的 y [ s a [ i − 1 ] + k ] y[sa[i - 1] + k] y[sa[i1]+k],则说明这俩层的子串的排名都都是相同的,否则第 i i i层的排名就比第 i − 1 i-1 i1层的排名要高(因为我们已经基础排序了,从小到大了),所以num++即可。当 n u m = n num=n num=n时,则说明有 n n n个不同的排名,因此就可以退出了。最后 m = n u m m=num m=num其实就是优化基数排序的值域。


最长公共前缀LCP

最长公共前缀是指两个字符串中长度最大的公共前缀,例如 s 1 = s_1= s1=" a b c x d abcxd abcxd", s 2 = s_2= s2=" a b c d e f abcdef abcdef",那么 L C P ( s 1 , s 2 ) = LCP(s_1,s_2)= LCP(s1,s2)=" a b c abc abc",其长度为3。

对于字符串 s = s= s=" a a b a a a a b aabaaaab aabaaaab", s u f f i x ( s a [ i ] ) suffix(sa[i]) suffix(sa[i])表示从第 s a [ i ] sa[i] sa[i]个字符开始的后缀,其排名为 i i i。例如 s a [ 3 ] = 5 sa[3]=5 sa[3]=5 s u f f i x ( s a [ 3 ] ) = suffix(sa[3])= suffix(sa[3])=" a a b aab aab",表示从第5个字符开始的后缀,其排名为3。

h e i g h t height height表示排名相邻的两个后缀的最长公共前缀的长度, h e i g h t [ 2 ] = 3 height[2]=3 height[2]=3表示排名第2的后缀和排名前一个后缀的最长公共前缀的长度为3。

如下图:

image-20210812155643145

h e i g h t [ i ] height[i] height[i]表示 s u f f i x ( s a [ i ] ) suffix(sa[i]) suffix(sa[i]) s u f f i x ( s a [ i − 1 ] ) suffix(sa[i-1]) suffix(sa[i1])的最长公共前缀的长度。

**性质1:**对于任意两个后缀 s u f f i x ( i ) 、 s u f f i x ( j ) suffix(i)、suffix(j) suffix(i)suffix(j),如果 r a n k [ i ] < r a n k [ j ] rank[i]<rank[j] rank[i]<rank[j],则它们的最长公共前缀的长度为 h e i g h t [ r a n k [ i ] + 1 ] , h e i g h t [ r a n k [ i ] + 2 ] , ⋯ , h e i g h t [ r a n k [ j ] ] height[rank[i]+1],height[rank[i]+2],\cdots,height[rank[j]] height[rank[i]+1]height[rank[i]+2]height[rank[j]]的最小值

例如, s u f f i x ( 4 ) = suffix(4)= suffix(4)=" a a a b aaab aaab", s u f f i x ( 1 ) = suffix(1)= suffix(1)=" a b a a a b abaaab abaaab", r a n k [ 4 ] = 2 rank[4]=2 rank[4]=2 r a n k [ 1 ] = 6 rank[1]=6 rank[1]=6,它们的最长公共前缀长度为 h e i g h t [ 3 ] height[3] height[3] h e i g h t [ 4 ] height[4] height[4] h e i g h t [ 5 ] height[5] height[5] h e i g h t [ 6 ] height[6] height[6]的最小值。

如下图所示:

image-20210812155704904

那么我们该如何求解 h e i g h t height height数组呢?如果两两比较,则时间复杂度为 O ( n 2 ) O(n^2) O(n2);若利用它们之间的关系进行线性递推,则时间复杂度为 O ( n ) O(n) O(n)

定义 h h h数组, h [ i ] = h e i g h t [ r a n k [ i ] ] h[i]=height[rank[i]] h[i]=height[rank[i]],其中 h [ i ] h[i] h[i]的含义: i i i个后缀(下标为 i i i开始的后缀)和排名在第 i i i个后缀前面的那个后缀最长公共前缀

根据 r a n k [ ] rank[] rank[] s a [ ] sa[] sa[]的互逆性, r a n k [ 3 ] = 1 rank[3]=1 rank[3]=1 h [ 3 ] = h e i g h t [ r a n k [ 3 ] ] = h e i g h t [ 1 ] = 0 h[3]=height[rank[3]]=height[1]=0 h[3]=height[rank[3]]=height[1]=0 r a n k [ 4 ] = 2 rank[4]=2 rank[4]=2 h [ 4 ] = h e i g h t [ r a n k [ 4 ] ] = h e i g h t [ 2 ] = 3 h[4]=height[rank[4]]=height[2]=3 h[4]=height[rank[4]]=height[2]=3。实际上,height[]h[]只是下标不同而已,前者使用 r a n k rank rank作为下标,后者使用 s a sa sa作为下标。

如下图所示:

image-20210812155728075

性质2: h [ i ] ≥ h [ i − 1 ] − 1 h[i]\geq h[i-1]-1 h[i]h[i1]1

有了这个性质,我们就可以枚举 i i i从1到 n n n,枚举以 i i i结尾的后缀,先求解出 h [ i − 1 ] h[i-1] h[i1],设 k = h [ i − 1 ] k=h[i-1] k=h[i1],如果 k > 0 k>0 k>0,则k--,也就是 h [ i − 1 ] − 1 h[i-1]-1 h[i1]1了,然后得到新的 k k k,最后把这个 k k k赋值给 h [ i ] h[i] h[i]就好了。我们在求出 h [ i − 1 ] − 1 h[i-1]-1 h[i1]1的基础上继续计算 h [ i ] h[i] h[i]即可,没有必要再从 1 1 1开始枚举了。

完美图解:

(1) i = 0 i=0 i=0

  • 先将下标转换为排名, r a n k [ 0 ] = 4 rank[0]=4 rank[0]=4
  • 求它的前一名,于是 r a n k [ 0 ] − 1 = 4 − 1 = 3 rank[0]-1=4-1=3 rank[0]1=41=3
  • 将前一名转换为下标, s a [ 3 ] = 5 sa[3]=5 sa[3]=5,因此 j = 5 j=5 j=5
  • k = 0 k=0 k=0开始比较,如果 s [ i + k ] = = s [ j + k ] s[i+k]==s[j+k] s[i+k]==s[j+k],那么k++,在比较结束时, k = 3 k=3 k=3,那么 h e i g h t [ r a n k [ 0 ] ] = h e i g h t [ 4 ] = k = 3 height[rank[0]]=height[4]=k=3 height[rank[0]]=height[4]=k=3

如下图所示

image-20210812155751087

(2) i = 1 i=1 i=1

  • 先将下标转换为排名, r a n k [ 1 ] = 6 rank[1]=6 rank[1]=6
  • 求它的前一名,于是 r a n k [ 1 ] − 1 = 6 − 1 = 5 rank[1]-1=6-1=5 rank[1]1=61=5
  • 将前一名转换为下标, s a [ 5 ] = 6 sa[5]=6 sa[5]=6,因此 j = 6 j=6 j=6
  • 此时 k = 3 , k ≠ 0 k=3,k\neq0 k=3,k=0,因此从上一次的运算结果 k − 1 k-1 k1开始接着比较,其实也就是 k = h [ i − 1 ] k=h[i-1] k=h[i1] k − 1 k-1 k1就相当于 h [ i − 1 ] − 1 h[i-1]-1 h[i1]1,所以此时新的 k = 2 k=2 k=2,因为 s [ i + k ] ≠ s [ j + k ] s[i+k]\neq s[j+k] s[i+k]=s[j+k],则 k k k不能增加,即此时不能在对比这两个后缀了,因此 h e i g h t [ 6 ] = 2 height[6]=2 height[6]=2

如下图所示

image-20210812155814628

(3)继续求解 i = 2 , 3 , ⋯   , n i=2,3,\cdots,n i=2,3,,n,即可得到所有的height[]

代码:

void get_Height()
{
    //先初始化rk
    for(int i=1;i<=n;i++)
        rk[sa[i]]=i;
    //i从1到n,枚举以下标i开始的后缀
    for(int i=1,k=0;i<=n;i++)
    {
        //这里的k相当于h[i-1] 如果k>0,那么k-1就相当于h[i-1]-1
        if(k)
            k--;
        //根据h[i]的定义可知,现在是在枚举下标i开始的后缀
        //rk[i]-1表示排名在 以下标i开始的后缀 的前面那个后缀的排名
        //那么sa[rk[i]-1]就可以得到 前面那个后缀 的 下标j
        int j=sa[rk[i]-1];
        //依次扫描对比这两个后缀,如果i+k下标不超过n,并且j+k下标不超过n
        //并且扫描到的字符都相等s[i+k]==s[j+k],则让k继续走
        while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k])
            k++;
        //我们知道h[i]=height[rk[i]],而这里的k是更新过后的h[i-1]-1,因此height[rk[i]]=k就相当于
        //h[i]=h[i-1]-1
        height[rk[i]]=k;
    }
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卷心菜不卷Iris

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值