zoukankan      html  css  js  c++  java
  • 后缀数组入门(一)——后缀排序

    前言

    后缀数组这个东西早就有所耳闻,但由于很难,学了好几遍都没学会。

    最近花了挺长一段时间去研究了一下,总算是勉强学会了用倍增法来实现后缀排序(据说还有一种更快的(DC3)法,但是要难得多)。

    数组定义

    首先,为方便起见,我们用后缀(_i)表示从下标(i)开始的后缀。(相信大家都知道后缀是什么的)

    首先,我们需要定义几个数组:

    (s):需要进行后缀排序的字符串。

    (SA_i):记录排名为(i)的后缀的位置

    (rk_i):记录后缀(_i)的排名

    (pos_i):同样记录排名为(i)的后缀的位置

    (tot_i):用于基数排序,统计(i)的排名

    要注意理解这些数组的定义,这样才能明白后面的内容。

    第一次操作

    首先,让我们来一步步模拟一下第一次操作。

    我们第一步是要将每个后缀按照第(1)个字符进行排序。

    这应该还是比较简单的,不难发现可以初始化得到(rk_i=s_i,pos_i=i)

    然后我们对其进行第一次排序。

    注意,排序最好用(O(n))基数排序,用(sort)的话会多一个(log)

    具体的一些关于基数排序的细节可以见下。

    关于基数排序

    后缀排序中的基数排序,其实相当于将二元组((rk_i,pos_i))进行排序。

    首先,第一步自然是清空(tot)数组。

    然后,从(1)(n)枚举,将(tot_{rk_i})(1)

    接下来是一遍累加,求出每一个元素的排名。

    然后从(n)(1)倒序枚举,更新(SA)数组即可。

    接下来的操作

    接下来自然是要对每个后缀前(2)个字符进行排序了。

    暴力的方法就是再重新排序一遍。

    但实际上,在确定了第(1)个字符的大小关系后,我们就不需要如此麻烦了。

    因为后缀(_i)的第(2)个字符,实际上就是后缀(_{i+k})的第(1)个字符。

    因此我们通过第一次排序,就可以直接确定第(2)个字符的大小关系了。

    于是我们就可以重新用(pos)数组将这个大小关系记录下来,再次排序。

    然后就是按照这种方法来倍增处理第(4)个字符、第(8)个字符、第(16)个字符... ...

    重复此操作直至所有后缀各不相同即可。

    这样的总复杂度就是(O(nlogn))的了。

    具体实现还是有很多细节的,实在没理解的可以根据代码再研究一下。

    代码(板子题

    class Class_SuffixSort//后缀排序
    {
        private:
            int n,SA[N+5],rk[N+5],pos[N+5],tot[N+5];
            inline void RadixSort(int S)//基数排序,S表示字符集大小
            {
                register int i;
                for(i=0;i<=S;++i) tot[i]=0;//清空数组
                for(i=1;i<=n;++i) ++tot[rk[i]];//从1到n枚举,将tot[rk[i]]加1
                for(i=1;i<=S;++i) tot[i]+=tot[i-1];//累加
                for(i=n;i;--i) SA[tot[rk[pos[i]]]--]=pos[i];//倒序枚举,更新SA数组
            }
        public:
            inline void Solve(char *s)
            {
                register int i,k,cnt=0,Size=122;//初始化字符集大小为122(即'z'的ASCII码)
                for(n=strlen(s),i=1;i<=n;++i) rk[pos[i]=i]=s[i-1];//初始化rk数组和pos数组
                for(RadixSort(Size),k=1;cnt<n;Size=cnt,k<<=1)//先是一遍基数排序,然后倍增枚举k,直至所有后缀各不相同
                {
                    for(cnt=0,i=1;i<=k;++i) pos[++cnt]=n-k+i;//将长度小于等于k的后缀先加入数组中,此时的cnt相当于计数器
                    for(i=1;i<=n;++i) SA[i]>k&&(pos[++cnt]=SA[i]-k);//对于排名大于k的字符串,将其加入数组中
                    for(RadixSort(Size),i=1;i<=n;++i) pos[i]=rk[i];//基数排序一遍,然后将rk数组的值全部赋值给pos数组
                    for(rk[SA[1]]=cnt=1,i=2;i<=n;++i) rk[SA[i]]=(pos[SA[i-1]]^pos[SA[i]]||pos[SA[i-1]+k]^pos[SA[i]+k])?++cnt:cnt;//利用SA数组来得到rk,此时的cnt存储不同的字符串个数,从而得到排名
                }
                for(i=1;i<=n;++i) F.write(SA[i]),F.writec(' ');//输出答案
            }
    }S;
    
  • 相关阅读:
    1062 Talent and Virtue (25 分)
    1083 List Grades (25 分)
    1149 Dangerous Goods Packaging (25 分)
    1121 Damn Single (25 分)
    1120 Friend Numbers (20 分)
    1084 Broken Keyboard (20 分)
    1092 To Buy or Not to Buy (20 分)
    数组与链表
    二叉树
    时间复杂度与空间复杂度
  • 原文地址:https://www.cnblogs.com/chenxiaoran666/p/SuffixSort.html
Copyright © 2011-2022 走看看