后缀数组(SA)学习笔记

浅谈后缀数组

后缀数组是个啥?

后缀数组嘛,准确的来说应该叫做后缀排序

就是对一个字符串的所有后缀进行排序啦


后缀数组怎么写?

前置芝士:基数排序

num[i]:原有数组
c[i]:桶
p[i]:排序过后的东西,代表第i位的是多少
n:num数量
m:num的种类(范围)
for(int i=1;i<=n;++i)c[num[i]]++;
for(int i=1;i<=m;++i)c[i]+=c[i-1];
for(int i=1;i<=n;++i)p[c[num[i]]--]=i;

这种排序是\(O(n)\)的,适用于数据种类比较小的情况下,例如只包含小写字母的字符串。


后缀排序

最暴力的解法:直接对每一个后缀进行排序,时间复杂度\(O( n^2 \log_2 n)\)

然而这样很明显会T飞

然后我们在求后缀排序的时候有两种办法,DC3与倍增

但我不会DC3

所以我就讲讲倍增吧

大概是这样的:

定义后缀i代表由第i位开始的后缀

那么,有了基数排序,且我们知道字符集一般是比较小的,我们就可以对每一个后缀的第一位进行排序,得到第一个结果

然后给每一位一个值,这是它的第一关键字,设为x[i]

进入循环。首先对于第1~n-1位,设置一个第二关键字的位置,设为y[i]。那么这一次的第二关键字的位置就分别为2~n

第n位设置的第二关键字是最小的,因为它本不应该有第二关键字

然后再根据第二关键字进行基数排序,然后如果每一个都不一样,即排名的最大值到达了总量,我们就把它输出

如果不行,重复上述步骤,不断扩展比较的位数,扩展为4位,第二关键字的位置应为2~n-1,每一个第二关键字都是上一轮排出来的结果

然后再基数排序再检查,直到没有重复的排名,我们就可以输出了

这里就可以用倍增了,因为每一次扩展的位数都是指数级增长的

从而后缀数组达到了log级别的复杂度

总时间复杂度\(O( n \log_2 n)\),常数是比DC3要小的

上代码:

#include<bits/stdc++.h>
using namespace std;
int m=122;//字符集的大小,这里设置位122就可以直接把大写与小写字母都包含进去
int n;//字符串长度
char s[2000001];//字符数组
int c[2000001];//一个桶
int x[2000001];//一开始用于把字符数组转化为int类型,之后用于存储排名为i的后缀的位置
int y[2000001];//基数排序的第二关键字,即当前正在比较的字符串的权值
int sa[2000001];//第i个后缀的排名
void getsa(){
    for(int i=1;i<=n;++i)c[x[i]=s[i]]++;
    for(int i=2;i<=m;++i)c[i]+=c[i-1];
    for(int i=n;i>=1;--i)sa[c[x[i]]--]=i;//桶排
    for(int k=1;k<=n;k<<=1){//倍增,k代表每一个后缀的长度
        int num=0;//初始化,num代表有多少个排名不等的后缀
        for(int i=n-k+1;i<=n;++i)y[++num]=i;//对于每一个后缀,把它的第二关键字设置为它后面的k/2个字符,第k+1个以及往后的后缀都无法加入这个第二关键字
        for(int i=1;i<=n;++i)if(sa[i]>k)y[++num]=sa[i]-k;//如果上一轮的排名比k高,就把它作为第二关键字。这里可以保证如果上一轮的排名比K大的话那么这个第二关键字没鬼用,因为别人的第二关键字都大于n-k+1,而这里的却肯定小于n-k+1,因为sa[i]肯定是小于k的
        //然后显然num肯定是n
        for(int i=1;i<=m;++i)c[i]=0;//清空桶
        for(int i=1;i<=n;++i)c[x[i]]++;
        for(int i=2;i<=m;++i)c[i]+=c[i-1];
        for(int i=n;i>=1;--i)sa[c[x[y[i]]]--]=y[i],y[i]=0;//以y[i]为权值再次排序
        swap(x,y);//x,y交换,此时x是全部为0,y变成了x,即上一轮排名为i的后缀的位置
        x[sa[1]]=num=1;//初始化,上一轮排名为1的后缀位置设置为1
        for(int i=2;i<=n;++i){
            x[sa[i]]=(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k])?num:++num;//如果上一轮中,第i名与第i-1名不相同且第i名的位置+k与第i-1名的位置+k相同,那么上一轮排名为i的就与上一轮排名为i-1的不一样了
        }
        if(num==n)break;//num==n时就已经把n个字符都排完了嘛,然后就break
        m=num;//把m变小一点优化一下常数
    }
    for(int i=1;i<=n;++i)printf("%d ",sa[i]);
}
int main(){
    scanf("%s",s+1);
    n=strlen(s+1);
    getsa();
}

LCP

LCP,就是指最长公共前缀

这里定义\(LCP(i,j)\)为后缀i与后缀j的最长公共前缀

首先定义一下h[i],意思即\(LCP(i-1,i)\)

有一个结论,叫做\(h[i]>=h[i-1]-1\)

证明如下:

首先我们不妨设第i-1个字符串按排名来的前面的那个字符串是第k个字符串,注意k不一定是i-2,因为第k个字符串是按字典序排名来的i-1前面那个,并不是指在原字符串中位置在i-1前面的那个第i-2个字符串。

这时,依据height[]的定义,第k个字符串和第i-1个字符串的公共前缀自然是height[rk[i-1]],现在先讨论一下第k+1个字符串和第i个字符串的关系。

第一种情况,第k个字符串和第i-1个字符串的首字符不同,那么第k+1个字符串的排名既可能在i的前面,也可能在i的后面,但没有关系,因为height[rk[i-1]]就是0了呀,那么无论height[rk[i]]是多少都会有height[rk[i]]>=height[rk[i-1]]-1,也就是h[i]>=h[i-1]-1。

第二种情况,第k个字符串和第i-1个字符串的首字符相同,那么由于第k+1个字符串就是第k个字符串去掉首字符得到的,第i个字符串也是第i-1个字符串去掉首字符得到的,那么显然第k+1个字符串要排在第i个字符串前面。同时,第k个字符串和第i-1个字符串的最长公共前缀是height[rk[i-1]],

那么自然第k+1个字符串和第i个字符串的最长公共前缀就是height[rk[i-1]]-1。

到此为止,第二种情况的证明还没有完,我们可以试想一下,对于比第i个字符串的排名更靠前的那些字符串,谁和第i个字符串的相似度最高(这里说的相似度是指最长公共前缀的长度)?显然是排名紧邻第i个字符串的那个字符串了呀,即sa[rank[i]-1]。但是我们前面求得,有一个排在i前面的字符串k+1,LCP(rk[i],rk[k+1])=height[rk[i-1]]-1;

又因为height[rk[i]]=LCP(i,i-1)>=LCP(i,k+1)

所以height[rk[i]]>=height[rk[i-1]]-1,也即h[i]>=h[i-1]-1。

来自\(click\)

那么根据这个结论就可以直接求h[i]了

void geth(){
    int k=0;
    for(int i=1;i<=n;++i)rk[sa[i]]=i;//排名为sa[i]的就是第i个后缀
    for(int i=1;i<=n;++i){
        if(rk[i]==1)continue;//第一名和第0名的LCP肯定是0啊
        if(k)--k;//从h[i-1]-1开始枚举
        int j=sa[rk[i]-1];//与自己排名相差1的那一个后缀
        while(j+k<=n&&i+k<=n&&s[i+k]==s[j+k])++k;//枚举
        height[rk[i]]=k;
    }
}

有了每一个的h[i],即\(LCP(i-1,i)\),我们就可以求出来任意的\(LCP(i,j)\)

\(i<=j\)

显而易见\(LCP(i,j)=min(LCP(i,i+1),LCP(i+1,i+2),...LCP(j-1,j))\)

可以套用RMQ或者其它快速查询的东西辅助求出


做的一些题目的题解

咕咕咕

转载于:https://www.cnblogs.com/youddjxd/p/10716563.html

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值