后缀数组入门

本文详细介绍了后缀数组的定义及计算方法,并通过示例说明了如何使用Manber和Myers算法进行计算。此外,还介绍了高度数组的概念及其构建方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

(一)后缀数组

  1. 后缀数组(Suffix Array)指的是将某个字符串的所有后缀按字典排序后得到的数组。不过数组中并不需要直接保存所有的后缀字符串,只要记录对应的起始位置就好了。我们用 S [ i . . . ] S[i...] S[i...]来表示字符串 S S S从位置 i i i开始的后缀。

    "abracadabra"对应的后缀数组sa
isa[i]S[sa[i]…]
011空字符串
110a
27abra
30abracadabra
43acadabra
55adabra
68bra
71bracadabra
84cadabra
96dabra
109ra
112racadabra
  1. 后缀数组的计算
    计算长度为 n n n的字符串 S S S的后缀数组。下面就给大家介绍其中较为简单的一种——由Manber和Myers发明的 O ( n l o g 2 n ) O(nlog^2n) O(nlog2n)复杂度的算法。
    该算法的基本思想是倍增。首先计算从每个位置开始的长度为2的子串的顺序,再利用这个结果计算长度为4的子串的顺序,接下来计算长度为8的子串的顺序,不断倍增,直到长度大于等于 n n n就得到了后缀数组。长度为1的子串的顺序不需要计算,直接用字符的 A S C I I ASCII ASCII编码就可以得出顺序。
    我们用 S [ i , k ] S[i,k] S[i,k]来表示从位置 i i i开始的长度为 k k k的字符串子串。其中,当从位置 i i i开始,剩余字符不足k个时,表示的是从位置 i i i开始到字符串末尾的子串。
    图1
    计算abracadabra的后缀数组的过程

现在假设已经求得了长度为 k k k的子串的顺序,要求长度为 2 k 2k 2k的子串的顺序。记 r a n k k ( i ) rank_k(i) rankk(i) S [ i , k ] S[i,k] S[i,k]在所有排好序的长度为 k k k的字串中是第几小的。要计算长度为 2 k 2k 2k的子串的顺序,就只要对两个 r a n k rank rank组成的数对进行排序就好了。我们通过对 r a n k k ( i ) rank_k(i) rankk(i) r a n k k ( i + k ) rank_k(i+k) rankk(i+k)的数对和 r a n k k ( j ) rank_k(j) rankk(j) r a n k k ( j + k ) rank_k(j+k) rankk(j+k)的数对的比较来替代对 S [ i , 2 k ] S[i,2k] S[i,2k] S [ j , 2 k ] S[j,2k] S[j,2k]的直接比较。

int n, k;
int rk[MAX_N + 1];
int tmp[MAX_N + 1];
//比较(rank[i], rank[i + k]) 和 (rank[j], rank[j + k])
bool compare_sa(int i, int j)
{
    if(rk[i] != rk[j])
        return rk[i] < rk[j];
    else
    {
        int ri = i + k <= n ? rk[i + k] : -1;
        int rj = j + k <= n ? rk[j + k] : -1;
        return ri < rj;
    }
}

void construct_sa(string S, int *sa)
{
    n = S.length();
    for(int i = 0; i <= n; i++)
    {
        sa[i] = i;
        rk[i] = i < n ? S[i] : -1;
    }
	//利用长度为k的排序结果对长度为2k的排序
    for(int k = 1; k <= n; k *= 2)
    {
        sort(sa, sa + n + 1, compare_sa);
        //先在tmp中临时存储新计算的rank,再转存回rank中
        tmp[sa[0]] = 0;
        for(int i = 1; i <= n; i++)
        {
            tmp[sa[i]] = tmp[sa[i - 1]] + (compare_sa(sa[i - 1], sa[i]) ? 1 : 0);
        }
        for(int i = 0; i <= n; i++)
        {
            rk[i] = tmp[i];
        }
    }
}

(二)高度数组

  1. 高度数组指的是后缀数组中相邻两个后缀的最长公共前缀(LCP, Longest Common Prefix)的长度组成的数组。记后缀数组为 s a sa sa,高度数组为 l c p lcp lcp,则有后缀 S [ s a [ i − 1 ] . . . ] S[sa[i - 1]...] S[sa[i1]...] S [ s a [ i ] . . . ] S[sa[i]...] S[sa[i]...]的最长公共前缀的长度为 l c p [ i ] lcp[i] lcp[i]。记 r a n k [ i ] rank[i] rank[i]为位置 i i i开始的后缀在后缀数组中的顺序,即有 r a n k [ s a [ i ] ] = i rank[sa[i]] = i rank[sa[i]]=i。这里的 r a n k rank rank数组与前面的 r a n k rank rank数组其实是一样的。

  2. 我们从位置0的后缀开始,从前往后依次计算后缀 S [ i . . . ] S[i...] S[i...]与后缀 S [ s a [ r a n k [ i ] − 1 ] . . . ] S[sa[rank[i] - 1]...] S[sa[rank[i]1]...](即后缀数组中的前一个后缀)的最长公共前缀的长度。

void construct_lcp(string S, int *sa, int *lcp)
{
    int n = S.length();

    int h = 0;
    lcp[0] = 0;
    for(int i = 0; i < n; i++)
    {
    	//得到从位置i开始的后缀的前一个后缀在字符串的开始位置
        int j = sa[rk[i] - 1];
        if(h > 0)
            h--;
        for(; j + h < n && i + h < n; h++)
        {
            if(S[j + h] != S[i + h])
                break;
        }
        lcp[rk[i]] = h;
    }
}

以上内容来自《挑战程序设计竞赛》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值