浅谈后缀数组
后缀数组是个啥?
后缀数组嘛,准确的来说应该叫做后缀排序
就是对一个字符串的所有后缀进行排序啦
后缀数组怎么写?
前置芝士:基数排序
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。
那么根据这个结论就可以直接求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或者其它快速查询的东西辅助求出