后缀数组小结
后缀数组(SA)是解决一系列字符串问题的强有力的工具
后缀数组的本质其实是对字符串\(s\)的所有后缀按照字典序从小到大排序
比如说,对于字符串\(s="aabbabab"\),我们将它的所有后缀排序得到如下的结果
\(aabbabab\)
\(ab\)
\(abab\)
\(abbabab\)
\(b\)
\(bab\)
\(babab\)
\(bbabab\)
前置芝士
后缀
这玩意要说吗
计数排序
所有的基于比较大小的排序算法最优是\(O(nlogn)\)的,如果我们想要加速的话就必须考虑那些并不是基于比较大小的算法
计数排序是一个\(O(n+maxd)\)的稳定算法,其中\(maxd\)表示要排序的元素中的最大值
计数排序的思想主要是利用前缀和统计出比当前元素小的元素有多少个,比如说对于某一个元素\(a\)如果序列中有4个比它小的元素,那么\(a\)就应该排在第五名
在实现计数排序的时候一般需要三个数组
(1)\(tax\),作为一个桶,用来统计前缀和
(2)\(rnk\),储存用来排序的关键字大小(即原序列)
(3)\(ans\),第\(i\)位表示排名为\(i\)的元素是什么
首先在\(O(maxd)\)的时间内统计出\(tax\)数组
接着在\(O(n)\)的时间内统计出\(ans\),注意在每排出一个元素的位置之后将\(tax--\)
如果有双关键字的话同理,注意枚举顺序
rmq问题
一个简单的问题,给定一个序列\(A\),\(q\)个询问,每次询问区间\([l..r]\)的最小值
如果有修改的话那么这就是一道线段树
但是问题在于这个问题没有修改,于是我们可以用一些奇奇怪怪的方法搞过去
维护一个\(mind[i][p]\)表示区间起始点为\(i\),长度为\(2^w\)的一个区间的最小值
这个玩意可以递推得到,\(mind[i][p]=min(mind[i][p-1],mind[i+(1<<(p-1))][p-1]\)(类似于倍增),边界条件\(mind[i][0]=A[i]\)
同时处理一个\(low\)数组,表示\(log_2i\)(因为系统自带的log很慢)
然后在处理询问的时候先根据区间长度(\(r-l+1\))处理出相应的\(p\)
然后这一段区间的最小值就是\(min(mind[l][p],mind[r-(1<<p)+1][p])\)(考虑从\(l\)和\(r\)开头的区间将原区间覆盖)
模板
求sa数组
后缀数组的构造主要会用到四个数组
(1)\(sa\),第\(i\)位表示排名为\(i\)的后缀在原串中的位置(即首字母的出现位置)
(2)\(rnk\),第\(i\)位表示位置为\(i\)的后缀的字典序排名
那么很明显就会有
(3)\(tp\),辅助作用,主要是用来储存第二关键字和上一轮的rnk的临时储存
(4)\(tax\),计数排序的桶
构造后缀数组主要有倍增法\((O(nlogn))\)和DC3法\((O(n))\),由于前者除了时间复杂度以外都与第二个方法相比更为优秀,故在这里只讨论倍增法(又是倍增啊)
在使用倍增法的时候,我们假设我们已经完成了前\(2^w\)位的排序,接下来将要对\(2^w+1\text~2^{w+1}\)位排序
首先明确一点:在前\(2^w\)位中排在\(i\)之后的后缀,在完成\(2^{w+1}\)位排序之后是不会跑到\(i\)之前的
所以我们排序的第一个关键字就是当前的\(rnk\)
但是这明显是不够的,我们还需要一个第二关键字——以第\(2^w+1\text~2^{w+1}\)位排序得到的结果
我们将会用到\(tp\)——第\(i\)位表示排名为\(i\)的后缀的位置,作为第二关键字进行排序
首先排在\(tp\)最前面的就是那些没有后\(2^w\)位的后缀,这一些后缀的起始位置是\(n-w+i(i\leq w)\)
那么接下来就是对剩下的那些后缀排序,我们如何快速确定它们的顺序呢?
对于一个有后\(2^w\)位的后缀,这后\(2^w\)位不只是属于这一个后缀,他还会成为前\(2^w\)位,如果所有的后\(2^w\)位都变成了前\(2^w\)位,那么我们就可以用之前得到的\(rnk\)来确定顺序
对于一个起始位为\(i\)的后缀,如果它有后\(2^w\)位的话,那么这后\(2^w\)位应该是起始位置是\(i+2^w\)的后缀
经过这一转化之后,我们就可以利用\(rnk\)得到\(tp\)的值
得到了\(rnk\)和\(tp\)之后,我们就可以以它们为第一、二关键字进行计数排序,得到新的\(sa\)数组
那么我们就只剩下从新的\(sa\)推出新的\(rnk\)了
很明显由一开始的性质有\(rnk[sa[1]]=1\),那么接下来是否也可以这么递推呢?
很遗憾并不行,因为对于前\(2^{w+1}\)位如果有两个字符串出现相同的话,那么暂时我们认为这两个字符串是大小相等的,即它们的\(rnk\)均为一样的
对于这种情况,我们需要使用上一次的\(rnk\)数组,我们考虑在现在的\(sa\)中相邻的两个后缀,如果它们的前\(2^w\)位和后\(2^w\)位都是相同的话就说明它们在这一轮的排序中无法被区分,即\(rnk\)相同
而这两个都是可以利用上一次的\(rnk\)数组得到,我们先将上一次的\(rnk\)放入\(tp\)中,然后直接比较\(tp\)的大小
emmmm,讲的可能比较乱,直接上代码吧
void qsort()
{
int i;
for (i=0;i<=m;i++) tax[i]=0;
for (i=1;i<=n;i++) tax[rnk[i]]++;
for (i=1;i<=m;i++) tax[i]+=tax[i-1];
for (i=n;i>=1;i--) sa[tax[rnk[tp[i]]]--]=tp[i];
}
void get_sa()
{
memset(rnk,0,sizeof(rnk));
memset(sa,0,sizeof(sa));
memset(tp,0,sizeof(tp));
memset(tax,0,sizeof(tax));
int i,p=0,w;n=strlen(s+1);m=30;
for (i=1;i<=n;i++) {rnk[i]=s[i]-'a'+1;tp[i]=i;}
qsort();
for (w=1;p<n;w<<=1)//这里只要比较出了n个后缀的大小就算完成任务了,所以可以直接退出
{
p=0;
for (i=1;i<=w;i++) tp[++p]=n-w+i;
for (i=1;i<=n;i++)
if (sa[i]>w) tp[++p]=sa[i]-w;
qsort();
memcpy(tp,rnk,sizeof(tp));
rnk[sa[1]]=1;p=1;
for (i=2;i<=n;i++)
if ((tp[sa[i]]==tp[sa[i-1]]) && ((tp[sa[i]+w]==tp[sa[i-1]+w]))) rnk[sa[i]]=p;
else rnk[sa[i]]=(++p);
m=p;
}
}
求height数组
如果只是单纯的构造sa数组是体现不出后缀数组的强大作用的
我们知道,很多字符串题都是与lcp(最长公共前缀)或lcs(最长公共后缀)有关
于是利用\(sa\)和\(rnk\)这两个数组可以构建出一个新的数组——\(height\)
\(height[i]\)表示\(sa[i]\)与\(sa[i-1]\)的最长公共前缀
如果我们再记\(H[i]=height[rnk[i]]\)的话,就会有一个十分有意思的性质:\(H[i]\geq H[i-1]-1\)
证明的话,就还是移步这位dalao的博客吧,大体思路就是按照两个后缀的首字母是否相同进行分类讨论
博客链接(%%%dalao):https://www.cnblogs.com/zwfymqz/p/8413523.html
void get_h()
{
int i,pre=0;
for (i=1;i<=n;i++)
{
if (pre) pre--;
int j=sa[rnk[i]-1];
while ((i+pre<=n) && (j+pre<=n) && (s[i+pre]==s[j+pre])) pre++;
height[rnk[i]]=pre;
}
}
利用\(height\)数组我们可以处理一系列问题
例题先咕着,有时间再补qwq