【理论】后缀数组 I —— 后缀排序

后缀数组(suffix array)是省选字符串题目中非常重要的算法。

本文将简略讲述其 O(nlogn) 求法,对于时间复杂度更优秀但 not practical 的做法不作提及。

模板

考虑一种字符串比较大小的新方式。

对于长度为 n 的字符串 s1,s2,我们考虑先比较其前 k 位,若前 k 位已经能比较出大小,则比较成功,否则再比较其后 nk 位。

这种比较方式的正确性很显然,但我们要考虑其带给我们的启示。

我们这个比较方式实际上将一个大的问题分解为了两个相对较小的问题,这实际上是一种分治。

那么,基于这种分治思想的后缀排序便应运而生了。

其采用分治的思路,使用倍增进行具体的实现(先解决 2k1 规模的问题再通过合并求得 2k 规模问题的答案),这很容易理解,因为分成的两个相对较小的子问题的复杂度应相对平均,故实际上最优的 kn2 时取到。

由于在后缀排序中我们需要比较多个字符串,所以我们需要思考如何表示多个字符串大小关系,我们可以采用排名的方式,记录每个字符串排序后在有序的字符串序列中的位置,以此作为大小关系的表示方式,特别的,当两个字符串相等时,其排名也相等。

后缀排序的过程如下:

设字符串 s 的第 l 位至第 r 位构成的子串为 s[lr]

使用后缀数组进行长度为 2k 的所有字符串排序时,被排序的字符串为 s[12k],s[22k+1],s[32k+2]

在比较 s[x2k+x1]s[y2k+y1] 时,由于我们已经将所有长度为 2k1 的所有字符串的排名求出来了,记 s[z2k1+z1] 对应的排名为 rkz,则这两个字符串的比较方式为:

  • 先比较 rkxrky 的大小,若 rkxrky 则比较结果直接可得出。

  • rkx=rky 则再继续比较 rkx+2k1rky+2k1 的大小。

需要注意的是,如果经过两次比较后仍相同,它们的排名也需保持相同。

很多人在了解全过程后会有疑问,求的应该是后缀的排序,但过程中只有对所有长度为 2k 的字符串的排序,哪里体现出了后缀的排序呢?

事实上,最后一次倍增时有 2kn,此时子串的右端点 i+2k1 一定不小于 n,这样的话 s[ii+2k1] 可以直接代表一个后缀。

由此,后缀数组的过程便完成了,但如何在每一次倍增时根据 rkx,rky,rkx+2k1,rky+2k1 进行排序是我们需思考的。

观察一下多个字符串比较的本质,我们发现其本质上为一个双关键字排序

考虑朴素做法,即暴力 O(nlogn) 排序,加上倍增的 O(logn),总复杂度 O(nlog2n),在 106 的数据范围下通过较为吃力。

那么可以想见我们需要一种更快的排序,能够在 O(n) 的时间复杂度内完成双关键字排序。

我们选择使用基数排序

我们将一个包含双关键字的字符串看做一个二元组。

下面简述基数排序的过程:

1. 将所有第二关键字进行桶排序,由此得到按照第二关键字大小排列的二元组顺序。

2. 将所有第一关键字进行桶排序,并进行前缀和以获得当前排名(后面排名会不断刷新,此处的当前排名对应的是第一关键字为它且第二关键字最大的二元组)。

3. 倒序枚举根据第二关键字大小排列的二元组(倒序枚举保证了第二关键字有序),每枚举到一个二元组便将排名赋为对应桶上的排名并将该桶对应的排名 1(倒序枚举的用处体现,对于同第一关键字的二元组,先被枚举的排名一定大于等于后被枚举的排名,因为第二关键字有序)。

但我们发现,如此排完一次序后,所有二元组的排名将互异,但却存在两个二元组(字符串)相同的情况,怎么办?

考虑判断排序后相邻二元组是否相等,即赋排名时若该二元组与上一个二元组相同,则排名等于上一个二元组的排名,否则排名等于上一个二元组的排名 +1

后缀排序问题成功解决。

代码
void sufsrt(){
    rep(i,1,n) ++a[(ll)s[i]];
    rep(i,1,200) a[i]+=a[i-1];
    rep(i,1,n) sa[a[(ll)s[i]]--]=i;
    rk[sa[1]]=1;
    rep(i,2,n){
        rk[sa[i]]=rk[sa[i-1]];
        if(s[sa[i]]==s[sa[i-1]]) continue;
        ++rk[sa[i]];
    } 
    rep(i,0,n){
        rep(j,1,n) a[j]=0;
        rep(j,1,n) ++a[rk[f[i][j]]];
        rep(j,1,n) a[j]+=a[j-1];
        rep(j,1,n) tp[a[rk[f[i][j]]]--]=j;
        rep(j,1,n) a[j]=0;
        rep(j,1,n) ++a[rk[j]];
        rep(j,1,n) a[j]+=a[j-1];
        repp(j,n,1) sa[a[rk[tp[j]]]--]=tp[j];
        nw[sa[1]]=1;
        rep(j,2,n){
            nw[sa[j]]=nw[sa[j-1]];
            if(rk[sa[j]]==rk[sa[j-1]]&&rk[f[i][sa[j]]]==rk[f[i][sa[j-1]]]) continue;
            ++nw[sa[j]];
        }  
        rep(j,1,n) rk[j]=nw[j];
        if((1<<i)>=n) break;
    }
    return;
}
posted @   lstqwq  阅读(199)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 终于写完轮子一部分:tcp代理 了,记录一下
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
点击右上角即可分享
微信分享提示
主题色彩