字符串——后缀数组

字符串——后缀数组

后缀数组

记字符串 S S S 的后缀字符串 s i s_i si S [ i … n ] S[i \ldots n] S[in] 即以 i i i 为起点的后缀,将所有的后缀字符串按照字典序进行排序,得到后缀数组,后缀数组主要包含两个:

  1. s a [ i ] sa[i] sa[i] 排名为 i i i 的后缀字符串的起点。
  2. r k [ i ] rk[i] rk[i] i i i 为起点的后缀字符串的排名。

后缀排序

一种朴素的后缀排序算法的时间复杂度是 O ( n 2 log ⁡ n ) O(n^2 \log n) O(n2logn) 的,通过使用一种倍增的思想,我们能够实现 O ( n log ⁡ 2 n ) O(n \log^2 n) O(nlog2n) 的后缀排序算法。

第一步我们先处理所有子串长度为 1 1 1 r k rk rk 数组的排名,即字符的 ASCII 值本身,可以写作 r k [ i ] = s t r [ i ] rk[i] = str[i] rk[i]=str[i] ,接下来我们倍增进行扩大子串的长度为 2 , 4 , 8 , 16 , … 2,4,8,16,\ldots 2,4,8,16, ,我们已知 子串长度为 w w w s a sa sa r k rk rk 数组,我们可以通过双关键字排序求得子串长度为 2 w 2w 2w s a sa sa r k rk rk 数组。长度不足的子串使用空字符进行不全,易知在字符串尾部添加空字符串并不影响后缀子串的排名。

那么问题就转换成对字符串组 A B AB AB C D CD CD 进行比较大小,其中 A , B , C , D A,B,C,D A,B,C,D 都是长度为 w w w 的子串,很显然,我们先比较 A A A C C C 的大小,如果相等再比较 B B B D D D 的大小即可,即进行双关键字比较。

SA
根据上面的思路,我们的第一版代码为:

char str[1000005];
int sa[1000005];
int rk[1000005];
int nrk[1000005];

int cnt[1000005];
int idk[1000005];

void suffix_sort(int n)
{
    for (int i = 1; i <= n; i++)
    {
        rk[i] = str[i];
        sa[i] = i;
    }

    for (int w = 1; w <= n; w <<= 1)
    {
        sort(sa + 1, sa + 1 + n, [&](int a, int b){
        	if(rk[a] == rk[b]) return rk[a + w] < rk[b + w];
        	return rk[a] < rk[b];
        });

        for (int i = 1, p = 0; i <= n; i++)
        {
            if (rk[sa[i]] == rk[sa[i - 1]] && rk[sa[i] + w] == rk[sa[i - 1] + w])
            {
                nrk[sa[i]] = p;
            }
            else
            {
                nrk[sa[i]] = ++p;
            }
        }
        memcpy(rk, nrk, sizeof(rk));
    }
}

因为我们的排序值域较小,我们可以使用基数排序优化这个过程,下面的代码时间复杂度为 O ( n log ⁡ n ) O(n \log n) O(nlogn)


char str[1000005];
int sa[1000005];
int rk[1000005];
int nrk[1000005];

int cnt[1000005];
int idk[1000005];

// 后缀排序

void suffix_sort(int n)
{
    for (int i = 1; i <= n; i++)
    {
        rk[i] = str[i];
        sa[i] = i;
    }

    for (int w = 1; w <= n; w <<= 1)
    {
        memset(cnt, 0, sizeof(cnt));
        for (int i = 1; i <= n; i++)
            cnt[rk[sa[i] + w]]++;
        for (int i = 1; i <= 1000000; i++)
            cnt[i] += cnt[i - 1];
        for (int i = n; i >= 1; i--)
            idk[cnt[rk[sa[i] + w]]--] = sa[i];
        memcpy(sa, idk, sizeof(sa));

        memset(cnt, 0, sizeof(cnt));
        for (int i = 1; i <= n; i++)
            cnt[rk[sa[i]]]++;
        for (int i = 1; i <= 1000000; i++)
            cnt[i] += cnt[i - 1];
        for (int i = n; i >= 1; i--)
            idk[cnt[rk[sa[i]]]--] = sa[i];
        memcpy(sa, idk, sizeof(sa));

        for (int i = 1, p = 0; i <= n; i++)
        {
            if (rk[sa[i]] == rk[sa[i - 1]] && rk[sa[i] + w] == rk[sa[i - 1] + w])
            {
                nrk[sa[i]] = p;
            }
            else
            {
                nrk[sa[i]] = ++p;
            }
        }
        memcpy(rk, nrk, sizeof(rk));
    }
}

LCP

待补充

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值