后缀数组(上,SA与RANK)

见《下》:https://blog.csdn.net/fengqiyuka/article/details/95247518

背景
  • 在做一些字符串的题目时,我们总会遇到诸如此类字符串题目:
  • 23
  • 然后我们就发现,用什么KMP啊,什么trie啊,或是什么AC***的啊,都不能满足这道题的需求。
  • 于是(标题已经告诉我们了 ),我们就要引入一个新的东西。
后缀数组
  • 后缀数组,顾名思义,就是用来存储字符串后缀信息的数组。
  • 一般来说,所有后缀数组中最重要的有两个,一个是SA,一个是rank。
  • SA[i]是指字符串排名为i的后缀开头的位置,rank[i]是指以第i个位置为开头的后缀的排名
  • 例如在aba中,把所有后缀取出来,得aba,ba,a。
  • 排序后得:a,aba,ba。
  • 所以SA[1]=3,SA[2]=1,SA[3]=2。rank[1]=2,rank[2]=3,rank[3]=1。
  • 这个特别重要,因为以后很容易会弄混。
  • 并且有个显然的等式:SA[rank[i]]=i,rank[SA[i]]=i。
求法
  • 但我们有了这两个数组之后,并没有什么用处。要快速求出这两个数组才行!
  • 最暴力的做法就是直接用字符串去排序,时间复杂度是 O ( n 2 l o g 2 n ) O(n^2log_2n) O(n2log2n)
  • 但既然这么多字符串其实是挤在一个字符串里,它们必然有一些神奇的性质。
  • 比如——可以倍增。
倍增
  • 我们可以对于所有后缀,可以先按第一个字符排序,然后按前两个字符排序,紧接着是4个字符、8个字符…当达到倍增到某个 2 x > n 2^x>n 2x>n时,排序出来的必然等价于所有字符串直接排序。
  • 既然思路已经确定了,那我们就开始寻求如何通过 2 k − 1 2^{k-1} 2k1倍增到 2 k 2^k 2k
  • 首先,第一个字符是很好求的,因为即使你的字符串长度再大,字符集也不会大到哪里去,所以我们可以直接用一个基数排序搞定。
  • 然后,如果你已经知道了用前 2 k − 1 2^{k-1} 2k1个字符排序之后的顺序,你如何求出 2 k 2^k 2k呢。
  • 刚才的基数排序其实有启发性。我们可以把每一个后缀的前 2 k 2^k 2k个字符分成两半,以前半段为第一关键字,后半段为第二关键字来排序,比较显然这和直接用 2 k 2^k 2k个字符排序是一样的。
  • 之所以可以这样操作,是因为对于每一个后缀,由于前半段与后半段都在这个字符串内,它们的排名都可以很快地求出来。对于第 i i i个后缀,它在所有后缀中的排名显然是当前的 r a n k [ i ] rank[i] rank[i],而后半段在所有后缀中的排名则是当前的 r a n k [ i + 2 k − 1 ] rank[i+2^{k-1}] rank[i+2k1],假如 i + 2 k > n i+2^k>n i+2k>n,则补充为0。
  • 在快速地得出一二关键字的排名之后,我们就可以很方便地进行排序了。
  • 为了更好地理解,盗个图:
  • 在这里插入图片描述
基数排序
  • 其实用 O ( l o g 2 n ) O(log_2n) O(log2n)级别的排序也可以,但这样总时间复杂度就 O ( n l o g 2 2 n ) O(nlog_2^2n) O(nlog22n)的,很容易会被邪恶的出题人卡时间。
  • 我们注意到,当前的关键字只有2个,而且rank也不算大,用基数排序恰到好处。
  • 为了方便理解基数排序,这里有一堆数:
  • 283、203、681、682、592、321
  • 用基数排序是,我们先按最低关键字排序(为什么要先低后高呢?感性理解一下:后面才排的关键字对序列的顺序影响较先排的较大),这里的最低关键字是个位,所以我们先个位排序:
  • 681、321、682、592、283、203
  • 注意如果有两个数字它们的某一关键字相同,则按当前顺序,现在在前面的排在前面。比如假如按十位排的时候,有两个数的十位相同,则个位小的显然要排在前面,而因为之前已经按个位排过序了,所以个位小的显然在前面。
  • 然后按十位排序:
  • 203、321、681、682、683、592
  • 最后按百位排序:
  • 203、321、592、681、682、683
  • 我们发现这组数就这样排好了。

  • 让我们回到刚才的字符串排序。
  • 也是一样的,我们先按第二关键字排序,而后按第一关键字排序。
  • 简述一下每一个关键字排序的基本流程:
    • 记录当前关键字每一个数出现的次数 c o u n t [ i ] count[i] count[i]
    • 做一个前缀和,之所以要做这个东西是为了快速求出假如你的当前关键字为 i i i,你可以排到那个位置。
    • 不断插入,但要记住——假如当前关键字相同,在前面的依旧在前面。
  • 代码实现也比较简单。
小优化
  • 在对第二关键字排序的时候,我们发现完全可以不用这样3步走。
  • 因为对于非0第二关键字(即 i + 2 k &lt; = n i+2^k&lt;=n i+2k<=n),我们再上一次已经排过序了。
  • 显然我们只需要把补零的填充上去就可以了。
代码(丑)(P3809)
#include<cstdio>
#include<cstring>
using namespace std;
const int max=19;
char s[1100000];
int sa[1100000],rank[2100000],rank2[1100000],count[1100000],d[23],tp[2100000];
int main()
{
    scanf("%s",s+1);
    int n=strlen(s+1);
    for(int i=1;i<=n;i++) count[s[i]]++;
    for(int i=1;i<=1000;i++) count[i]+=count[i-1];
    for(int i=n;i>=1;i--){
        int t=s[i];
        sa[count[t]]=i;
        count[t]--;
    }
    for(int i=1;i<=n;i++){
        int t1=sa[i-1],t2=sa[i];
        if(s[t1]!=s[t2]) rank[t2]=rank[t1]+1;
        else rank[t2]=rank[t1];
    }
    d[0]=1;for(int i=1;i<=max;i++) d[i]=d[i-1]*2;
    for(int i=0;i<=max;i++){
        if(d[i]>n) break;
        int len=d[i];
        for(int j=1;j<=n;j++)
            if(sa[j]>d[i]) rank2[++len]=sa[j]-d[i];
        for(int j=1;j<=d[i];j++) rank2[d[i]+1-j]=n-d[i]+j;
        memset(count,0,sizeof(count));
        for(int j=1;j<=n;j++) count[rank[j]]++;
        for(int j=1;j<=n;j++) count[j]+=count[j-1];
        for(int j=n;j>=1;j--){
            int t=rank[rank2[j]];
            sa[count[t]]=rank2[j];
            count[t]--;
        }
        for(int j=1;j<=n;j++) tp[j]=rank[j];
        for(int j=1;j<=n;j++){
            int t1=sa[j-1],t2=sa[j];
            if(tp[t1]!=tp[t2]||tp[t1+d[i]]!=tp[t2+d[i]])
                rank[t2]=rank[t1]+1;
            else rank[t2]=rank[t1];
        }
    }
    for(int i=1;i<=n;i++) printf("%d ",sa[i]);
    printf("\n");
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值