后缀数组

后缀数组是处理字符串的有力工具。

实现方法主要是两种:倍增法 O ( n l o g n ) O(nlogn) O(nlogn)和DC3法 O ( n ) O(n) O(n)

本文主要介绍倍增法。


倍增法

定义第 i i i个后缀: s [ i … n ] s[i…n] s[in]。它的后缀位置为 i i i

定义变量:

s s s:原字符串。 s [ i ] s[i] s[i]:原字符串的第 i i i个字母。

n n n:字符串长度

m m m:字符集大小

s a [ i ] sa[i] sa[i]:排名为 i i i的后缀位置

r a k [ i ] rak[i] rak[i]:第 i i i个后缀的排名

x [ i ] x[i] x[i]:基数排序中第 i i i个后缀的第一关键字的排名

y [ i ] y[i] y[i]:基数排序中第二关键字排名为 i i i的后缀位置(即第 y [ i ] y[i] y[i]个后缀的第二关键词排名为 i i i)

c [ i ] c[i] c[i]:基数排序的桶

s a sa sa r a k rak rak可以互推: r a k [ s a [ i ] ] = i rak[sa[i]]=i rak[sa[i]]=i s a [ r a k [ i ] ] = i sa[rak[i]]=i sa[rak[i]]=i


接下来对后缀进行排序。

首先对所有后缀的第一个字母大小排名是能确定的,就是它的ASCII值。把第 i i i个字母看成 ( s [ i ] , i ) (s[i],i) (s[i],i)的二元组,对它进行基数排序就得到了后缀第一个字母的大小排名 x [ i ] x[i] x[i]

后缀的第二个字母的排名可以通过后缀的第一个字母排名计算。具体的, i i i个后缀的第2个字母就是第 i + 1 i+1 i+1个后缀的第1个字母,因此它们的排名相同。用第一个字母的排名和第二个字母的排名进行基数排序,得到所有长度为2的后缀的排名。

利用倍增的思想,接下来对每个后缀的前4个字母排序。上一轮得到前2个字母的排名;第 i i i个后缀的第3、4个字母就是第 i + 2 i+2 i+2个后缀的第1、2个字母,即得到了第 i i i个后缀后2个字母的排名。双关键字进行基数排序。

最后就能完成所有后缀的排序。整体排序过程:

后缀排序过程


具体代码解析

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 ;

第一行:第 i i i个后缀的第一个字母的排名 x [ i ] x[i] x[i]即为字符串第 i i i个字母: x [ i ] = s [ i ] x[i]=s[i] x[i]=s[i]

第二行:把排名(字母)对应的桶计数++,求前缀和,记录第一关键字排名为 i i i的排名范围。

第三行:第一次对 ( s [ i ] , i ) (s[i],i) (s[i],i)二元组进行基数排序,从大到小枚举第二关键字 i i i,找到它的排名对应的桶,这一二元组的排名即为该桶的大小。具体来说,对于相同的第一关键字,第二关键字 i i i越大,它就在相同的第一关键字中排名越后,即在第二关键字对应的第一关键字排名的桶中越靠后,即该桶的大小。找到排名后,将该桶的大小减一。


倍增部分:

for (int k = 1; k <= n; k <<= 1) {
    int num = 0 ;
    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 ;
    memset (c, 0, sizeof c) ;
    for (int i = 1; i <= n; i ++) c[x[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 ;
    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) return ;
    m = num ;
}
for (int k = 1; k <= n; k <<= 1)

倍增循环: k k k表示第一关键字和第二关键字长度为 k k k


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

n u m num num:第二关键字的排名计数

第二行:因为第 n − k + 1 n-k+1 nk+1到第 n n n个后缀没有第二关键字,因此它们的第二关键字排名最前(空串的排名最前),即排名为 n u m num num的第二关键字的后缀位置为 i i i。(参考 y y y数组定义)


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

枚举上一轮排序后后缀的排名,如果排名为 i i i的后缀位置大于 k k k,那么它可以作为别的后缀的后 k k k个字母。因为按照上一轮排序后后缀的排名枚举,所以该串作为别的后缀的后 k k k个字母排名一定靠前。记录排名为 n u m num num的第二关键字的后缀位置: k k k个字母的起始点为 s a [ i ] sa[i] sa[i],整个后缀的起始点即为 s a [ i ] − k sa[i]-k sa[i]k


memset (c, 0, sizeof c) ;
for (int i = 1; i <= n; i ++) c[x[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 ;

这行是对双关键字进行基数排序,比较难理解。

对比第一次双关键字基数排序:

for (int i = n; i; i --) sa[c[x[i]] --] = i ;

第一次双关键字排序是对 ( x [ i ] , i ) (x[i],i) (x[i],i)排序。第二关键字 i i i越大,对应第一关键字中的排名越靠后。

for (int i = n; i; i --) sa[c[x[y[i]]] --] = y[i], y[i] = 0 ;

此行同理。 i i i越大,第二关键字排名越大。 y [ i ] y[i] y[i]记录的是第二关键字排名为 i i i的后缀位置, x [ i ] x[i] x[i]记录的是后缀位置为 i i i的第一关键字排名。 x [ y [ i ] ] x[y[i]] x[y[i]]即第二关键字排名为 i i i对应的第一关键字排名第二关键字排名越靠后,在对应的第一关键字排名就越靠后,排名即为该对应第一关键字排名的桶的大小,即 c [ x [ y [ i ] ] ] c[x[y[i]]] c[x[y[i]]] s a [ i ] sa[i] sa[i]表示排名为 i i i的后缀位置。当前排名为 c [ x [ y [ i ] ] ] c[x[y[i]]] c[x[y[i]]],后缀位置为 y [ i ] y[i] y[i](参考 y [ i ] y[i] y[i]定义),即 s a [ c [ x [ y [ i ] ] ] ] = y [ i ] sa[c[x[y[i]]]]=y[i] sa[c[x[y[i]]]]=y[i]。最后将对应的第一关键字排名桶 c [ x [ y [ i ] ] ] c[x[y[i]]] c[x[y[i]]]的计数减一。 s a [ c [ x [ y [ i ] ] ] − − ] = y [ i ] sa[c[x[y[i]]]--]=y[i] 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;

此时要用之前的排序结果更新这一轮的排名 x [ i ] x[i] x[i] s a sa sa数组已在基数排序时更新完毕,利用 s a sa sa来更新 x x x

y y y数组已没用,因此直接交换 x x x y y y数组。或可以写成:

memcpy (y, x, sizeof x) ;

此时的 x x x可以看做 r a k rak rak数组。 x [ s a [ 1 ] ] = 1 x[sa[1]]=1 x[sa[1]]=1。(参考 s a sa sa r a k rak rak互推公式)

随后 f o r for for循环更新排名。因为可能会出现排名一样的情况,即前 k k k个字母排名相同,后 k k k个字母排名相同。

n u m num num统计出现了几个排名。

y [ s a [ i ] ] = = y [ s a [ i − 1 ] ] y[sa[i]]==y[sa[i-1]] y[sa[i]]==y[sa[i1]]并且 y [ s a [ i ] + k ] = = y [ s a [ i − 1 ] + k ] y[sa[i] + k] == y[sa[i-1]+k] y[sa[i]+k]==y[sa[i1]+k]时,即排名为 i i i的前 k k k个字母排名与排名为 i − 1 i-1 i1的前 k k k个字母排名一致,排名为 i i i的后 k k k个字母与排名为 i − 1 i-1 i1的后 k k k个字母排名一致,它们的总排名一致。否则排名为 n u m + 1 num+1 num+1,并把 n u m + + num++ num++

if (num == n) return ;
m = num ;

当排名个数等于 n n n,即每个后缀排名都不同时,后缀排序结束。

将字符集大小赋值成排名个数。


Luogu P3809【模板】后缀排序

#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e6 + 10 ;
char s[maxn] ;
int n, m ;
int sa[maxn], x[maxn], y[maxn], c[maxn] ;
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] ;
    for (int i = n; i; i --) sa[c[x[i]] --] = i ;
    for (int k = 1; k <= n; k <<= 1) {
        int num = 0 ;
        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 ;
        memset (c, 0, sizeof c) ;
        for (int i = 1; i <= n; i ++) c[x[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 ;
        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) return ;
        m = num ;
    }
}
int main() {
    scanf("%s", s + 1) ;
    n = strlen (s + 1); m = 128 ;
    get_sa () ;
    for (int i = 1; i <= n; i ++)
        printf("%d ", sa[i]) ;
    cout << endl ;
    return 0 ;
}

height数组

定义:

l c p ( x , y ) lcp(x,y) lcp(x,y):第 x x x个后缀和第 y y y个后缀的最长公共前缀

L C P ( x , y ) LCP(x, y) LCP(x,y):排名为 x x x的后缀与排名为 y y y的后缀的最长公共前缀

h e i g h t [ i ] height[i] height[i] l c p ( s a [ i − 1 ] , s a [ i ] ) lcp(sa[i-1],sa[i]) lcp(sa[i1],sa[i]) 排名为 i i i的后缀和排名为 i − 1 i-1 i1的后缀的最长公共前缀

H [ i ] H[i] H[i] h e i g h t [ r a k [ i ] ] height[rak[i]] height[rak[i]] i i i个后缀和它排名前一名的后缀的最长公共前缀


L C P LCP LCP的一些性质:

  1. L C P ( i , j ) = L C P ( j , i ) LCP (i,j)=LCP (j,i) LCP(i,j)=LCP(j,i)
  2. L C P ( i , i ) = l e n ( s u f f i x ( s a [ i ] ) ) = n − s a [ i ] + 1 LCP(i, i)=len (suffix(sa[i]))=n-sa[i]+1 LCP(i,i)=len(suffix(sa[i]))=nsa[i]+1
  3. L C P ( i , j ) = m i n i &lt; k ≤ j L C P ( k − 1 , k ) = m i n i &lt; k ≤ j h e i g h t [ k ] LCP(i, j)=min_{i \lt k \le j} LCP(k - 1, k)=min_{i \lt k \le j}height[k] LCP(i,j)=mini<kjLCP(k1,k)=mini<kjheight[k] i &lt; j i &lt; j i<j

H的性质: H [ i ] ≥ H [ i − 1 ] − 1 H[i] \ge H[i-1]-1 H[i]H[i1]1

证明:设第 i − 1 i-1 i1个后缀,排名在它前一位的是第 k k k个后缀。

根据 h e i g h t height height的定义,第 i − 1 i-1 i1个后缀和第 k k k个后缀的最长公共前缀是 h e i g h t [ r a k [ i − 1 ] ] height[rak[i-1]] height[rak[i1]]

讨论第 i i i个后缀和第 k + 1 k+1 k+1个后缀的关系。

  1. i − 1 i-1 i1个后缀的首字母与第 k k k个后缀的首字母不同。

    h e i g h t [ r a k [ i − 1 ] ] = 0 height[rak[i-1]]=0 height[rak[i1]]=0,那么 h e i g h t [ r a k [ i ] ] ≥ h e i g h t [ r a k [ i − 1 ] ] − 1 height[rak[i]] \ge height[rak[i-1]]-1 height[rak[i]]height[rak[i1]]1恒成立。

  2. i − 1 i-1 i1个后缀的首字母与第 k k k个后缀的首字母相同。

    i i i个后缀等于第 i − 1 i-1 i1个后缀去掉首字母,第 k + 1 k+1 k+1个后缀等于第 k k k个后缀去掉首字母。

    根据假设第 k k k个后缀排名在第 i − 1 i-1 i1个后缀的前一位,那么第 k + 1 k+1 k+1个后缀一定排在第 i i i个后缀的前面,即 r a k [ k + 1 ] &lt; r a k [ i ] rak[k+1] \lt rak[i] rak[k+1]<rak[i]

    因为第 i − 1 i-1 i1个后缀和第 k k k个后缀的最长公共前缀是 h e i g h t [ r a k [ i − 1 ] ] height[rak[i-1]] height[rak[i1]],所以第 i i i个后缀和第 k + 1 k+1 k+1个后缀的最长公共前缀就是 h e i g h t [ r a k [ i − 1 ] ] − 1 height[rak[i-1]]-1 height[rak[i1]]1

    与第 i i i个后缀拥有最长公共前缀的后缀一定是与第 i i i个后缀排名相邻的后缀,即为第 s a [ r a k [ i ] − 1 ] sa[rak[i]-1] sa[rak[i]1]个后缀

    ∵ r a k [ k + 1 ] ≤ r a k [ i ] − 1 &lt; r a k [ i ] , l c p ( i , k + 1 ) = h e i g h t [ r a k [ i − 1 ] ] − 1 \because rak[k+1] \le rak[i]-1 \lt rak[i], lcp (i, k + 1)=height[rak[i-1]]-1 rak[k+1]rak[i]1<rak[i],lcp(i,k+1)=height[rak[i1]]1

    ∴ l c p ( i , s a [ r a k [ i ] − 1 ] ) ≥ h e i g h t [ r a k [ i − 1 ] ] − 1 \therefore lcp (i, sa[rak[i]-1]) \ge height[rak[i-1]]-1 lcp(i,sa[rak[i]1])height[rak[i1]]1

    h e i g h t [ r a k [ i ] ] ≥ h e i g h t [ r a k [ i ] − 1 ] − 1 height[rak[i]] \ge height[rak[i]-1]-1 height[rak[i]]height[rak[i]1]1 H [ i ] ≥ H [ i − 1 ] − 1 H[i] \ge H[i-1]-1 H[i]H[i1]1

∴ \therefore 得证


构造方法:

void get_height () {
    for (int i = 1; i <= n; i ++) rak[sa[i]] = i ;
    int k = 0 ;
    for (int i = 1; i <= n; i ++) {
        if (k) k -- ;
        int j = sa[rak[i] - 1] ;
        while (s[j + k] == s[i + k]) k ++ ;
        height[rak[i]] = k ;
    }
}

应用

  1. 两个后缀的最大公共前缀

    l c p ( x , y ) = L C P ( r a k [ x ] , r a k [ y ] ) = m i n r a k [ x ] &lt; i ≤ r a k [ y ] h e i g h t [ i ] lcp (x, y)=LCP(rak[x], rak[y])=min_{rak[x] \lt i \le rak[y]}height[i] lcp(x,y)=LCP(rak[x],rak[y])=minrak[x]<irak[y]height[i],设 r a k [ x ] &lt; r a k [ y ] rak[x]&lt;rak[y] rak[x]<rak[y]

    R M Q RMQ RMQ维护 h e i g h t height height O ( 1 ) O(1) O(1)查询

  2. 可重叠最长重复子串: h e i g h t height height中的最大值

  3. 不可重叠最长重复子串

    二分答案 l e n len len,对 h e i g h t height height进行分组,要求每一组 h e i g h t height height的最小值 ≥ l e n \ge len len。统计每一组中 s a [ i ] sa[i] sa[i] s a [ i − 1 ] sa[i-1] sa[i1]的最小值和最大值, m a x − m i n ≥ l e n max-min \ge len maxminlen即合法。

  4. 本质不同的子串数量

    枚举后缀排名,排名为 i i i的后缀对答案的贡献为 n − s a [ i ] + 1 − h e i g h t [ i ] n-sa[i]+1-height[i] nsa[i]+1height[i]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值