后缀数组(SA)及height数组
最近感觉自己越来越蒟蒻了……后缀数组不会,费用流不会……
看着别人切一道又一道的题,我真是很无奈啊……
然后,我花了好长时间,终于弄懂了后缀数组。
后缀数组是什么?
后缀数组
给你一个字符串,让你将每个后缀排序,就是一个后缀数组。
比如,字符串为ababa
,就会搞出一个这样的东西:
a
aba
ababa
ba
baba
SA={4,2,0,3,1};
其中,每个后缀用开始的位置来表示。
数组
相当于逆着的,
#后缀数组怎么求?
方法一:暴力
打个选择排序,每次比较用的方法。
当然,这样的暴力出不了奇迹。
方法二:快排
仅仅是将选择排序变成快排罢了。
##方法三:倍增
倍增1.0
这就是本文的重点了!!!
想想其它的倍增是怎么做的,再想想字符串怎么倍增。
首先,给每个字符赋一个排名,像这样:
'a'->1
'b'->2
现在rank={1,2,1,2,1}
然后像下面的这幅图一样搞一搞。
倍增算法的精髓就在这幅图中。
枚举一个,对于每个位置,每次将和合并到一起(不够补0),然后排名(可以当做是离散化)。
这样就可以求出,然后求出。
具体没什么好讲的,只要看懂这幅图,就懂了倍增算法的思想。
这样就可以优化到了。
倍增2.0
然而,这不是倍增的极限。
可以用基数排序进一步地优化!!!
什么是基数排序,基数排序怎么打,请百度一下(基数排序可以用一维的数组来打,具体看代码实践)。
在此,我有意提醒的是,你可以将数看做一个m进制,在倍增合并两个数时,可以将其看做m进制的两位数,然后对它进行基数排序。
这样就有的时间复杂度了。
具体见代码。
DC3算法
笔者暂时不会……
后缀数组怎么打?
后缀数组其实是比较好理解的,但是,为了追求完美,我们不应光靠自己的理解打模板。
因为自己打的有时会非常丑陋……
看了,理解的网上的标,综合我自己的风格,就打出了个这样的标:
int y[2000003],ws[2000003],wv[2000003];
void getSA(char s[],int rank[],int sa[],int n,int m)
{
memset(ws,0,sizeof(int)*m);
memset(y,255,sizeof y);
memset(rank,255,sizeof rank);
for (int i=0;i<n;++i)
++ws[rank[i]=s[i]];
for (int i=1;i<m;++i)
ws[i]+=ws[i-1];
for (int i=n-1;i>=0;--i)
sa[--ws[rank[i]]]=i;
for (int i=1;p<n;i<<=1,m=p)
{
p=0;
for (int j=n-i;j<n;++j)
y[p++]=j;
for (int j=0;j<n;++j)
if (sa[j]>=i)
y[p++]=sa[j]-i;
for (int j=0;j<n;++j)
wv[j]=rank[y[j]];
memset(ws,0,sizeof(int)*m);
for (int j=0;j<n;++j)
++ws[wv[j]];
for (int j=1;j<m;++j)
ws[j]+=ws[j-1];
for (int j=n-1;j>=0;--j)
sa[--ws[wv[j]]]=y[j];
swap(rank,y);
p=1;
rank[sa[0]]=0;
for (int j=1;j<n;++j)
rank[sa[j]]=(y[sa[j-1]]==y[sa[j]] && y[sa[j-1]+i]==y[sa[j]+i]?p-1:p++);
}
这就是网上通常的打法,当然,风格会有些不一样……
是不是看了后,一头雾水?
别急,慢慢解释。
首先说一下,在这个程序中,取值是在范围内的,和上面那张图不一样!
memset(ws,0,sizeof(int)*m);
memset(y,255,sizeof y);
memset(rank,255,sizeof rank);
for (int i=0;i<n;++i)
++ws[rank[i]=s[i]];
for (int i=1;i<m;++i)
ws[i]+=ws[i-1];
for (int i=n-1;i>=0;--i)
sa[--ws[rank[i]]]=i;
前面三行赋初值。
这是处理最开始的和,也就是还没有合并时。
数组是一个桶,用于辅助基数排序。
注意第三行rank[i]=s[i]
。我们在实践的时候一开始不需要将真正的排名弄出来,我们只需知道它们的相对大小。而作为单个字符,是可以表示它们的相对大小的。
其它就没什么了,要理解好一维的基数排序!
for (int i=1,p=1;p<n;i<<=1,m=p)
表示的是对于一个位置,在这一轮中要用和合并。
表示不同的字符串的个数,初值设为是为了循环条件,显然时就没必要做了。
为什么循环条件是呢?因为我们发现,最后的数组一定是一个范围在的排列。
所以顶多为,想想,当时,那么其实已经排好序了,没必要再做下去,比如,可以看看上面那张图,可以发现最后一轮是没有必要的。
表示的也是不同字符串的个数,只是因为在下面要被用作计数器罢了。
p=0;
for (int j=n-i;j<n;++j)
y[p++]=j;
for (int j=0;j<n;++j)
if (sa[j]>=i)
y[p++]=sa[j]-i;
当初我看得最久的是这一段……
这其实是一个小优化。
是一个临时的数组。
在合并后,其实第二关键字可以通过上一次的数组求出。
先看看二、三行。显然,这段区间内,如果要和后面的合并,只能补,应该说是补,因为数组在这个程序中的取值是。
一定是最小的,所以先把它们排在前面。
然后看倒数三行,这个就比较难理解了。
对于位置,在这一轮中会对有影响,所以说,
因为是有序的,所以我们顺序枚举,将其中满足以上条件的加入中。
for (int j=0;j<n;++j)
wv[j]=rank[y[j]];
memset(ws,0,sizeof(int)*m);
for (int j=0;j<n;++j)
++ws[wv[j]];
for (int j=1;j<m;++j)
ws[j]+=ws[j-1];
for (int j=n-1;j>=0;--j)
sa[--ws[wv[j]]]=y[j];
这一段的作用就是以第一关键字来进行一次基数排序,和上面的那个一样的道理。
swap(rank,y);
p=1;
rank[sa[0]]=0;
for (int j=1;j<n;++j)
rank[sa[j]]=(y[sa[j-1]]==y[sa[j]] && y[sa[j-1]+i]==y[sa[j]+i]?p-1:p++);
起到计数器的作用。
重点是最后一行y[sa[j-1]]==y[sa[j]] && y[sa[j-1]+i]==y[sa[j]+i]
这是在比较和是否相等。如果相等,那么值应该要一样(不过注意,到最后时值一定是不同的!)
网上的标这样比较,就不怕爆掉吗?对此,我很不理解,只是开了两倍的数组来解决这个问题。
关于LCP
一些概念
表示和的公共最长前缀。
很明显,若,则
如何求?
首先,我们要知道一个性质:
设,也就是与它前一名的最长公共前缀。
那么
证明:
设表示前一名的后缀,即是它们的最长公共前缀。
当时,显然等式成立。
当时,可以发现和的公共后缀至少为。可以画张图理解一下。
所以,综上,
利用这个性质,我们可以在的时间内求出数组
代码
void getheight(char s[],int rank[],int sa[],int height[])
{
for (int i=0,k=0;i<n;++i)
if (rank[i])
{
if (k)
--k;
int j=sa[rank[i]-1];
while (s[i+k]==s[j+k])
++k;
height[rank[i]]=k;
}
}
其实不必真正地构出个数组。
其它
建议数组从开始,或者将及辅助数组初值设为,因为在比较时,后面的要补(或),我就因为这样调了很久……(被罗穗骞大佬的论文坑了)