博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
后缀数组(SA)及height数组
阅读量:4558 次
发布时间:2019-06-08

本文共 4141 字,大约阅读时间需要 13 分钟。

最近感觉自己越来越蒟蒻了……后缀数组不会,费用流不会……

看着别人切一道又一道的题,我真是很无奈啊……
然后,我花了好长时间,终于弄懂了后缀数组。

后缀数组是什么?

后缀SASASA数组

给你一个字符串,让你将每个后缀排序,就是一个后缀数组

比如,字符串为ababa,就会搞出一个这样的东西:

aabaabababababaSA={4,2,0,3,1};

其中,每个后缀用开始的位置来表示。

rankrankrank数组

相当于逆着的SASASArank[sa[i]]=irank[sa[i]]=irank[sa[i]]=i

#后缀数组怎么求?

方法一:O(N3)O(N^3)O(N3)暴力

打个选择排序,每次比较用O(N)O(N)O(N)的方法。

当然,这样的暴力出不了奇迹。

方法二:O(N2lg⁡N)O(N^2\lg N)O(N2lgN)快排

仅仅是将选择排序变成快排罢了。

##方法三:倍增

倍增1.0

这就是本文的重点了!!!

想想其它的倍增是怎么做的,再想想字符串怎么倍增。
首先,给每个字符赋一个排名,像这样:

'a'->1'b'->2

现在rank={1,2,1,2,1}

然后像下面的这幅图一样搞一搞。
倍增
倍增算法的精髓就在这幅图中。
枚举一个iii,对于每个位置jjj,每次将jjjj+2ij+2^ij+2i合并到一起(不够补0),然后排名(可以当做是离散化)。
这样就可以求出rankrankrank,然后求出SASASA
具体没什么好讲的,只要看懂这幅图,就懂了倍增算法的思想。
这样就可以优化到O(Nlg⁡2N)O(N\lg^2N)O(Nlg2N)了。

倍增2.0

然而,这不是倍增的极限。

可以用基数排序进一步地优化!!!
什么是基数排序,基数排序怎么打,请百度一下(基数排序可以用一维的数组来打,具体看代码实践)。
在此,我有意提醒的是,你可以将数看做一个m进制,在倍增合并两个数时,可以将其看做m进制的两位数,然后对它进行基数排序。
这样就有O(Nlg⁡N)O(N\lg N)O(NlgN)的时间复杂度了。
具体见代码。

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
=0;--i) sa[--ws[rank[i]]]=i; for (int i=1;p
<<=1,m=p) {
p=0; for (int j=n-i;j
=i) y[p++]=sa[j]-i; for (int j=0;j
=0;--j) sa[--ws[wv[j]]]=y[j]; swap(rank,y); p=1; rank[sa[0]]=0; for (int j=1;j

这就是网上通常的打法,当然,风格会有些不一样……

是不是看了后,一头雾水?
别急,慢慢解释。
首先说一下,在这个程序中,rankrankrank取值是在[0,n)[0,n)[0,n)范围内的,和上面那张图不一样!

memset(ws,0,sizeof(int)*m);	memset(y,255,sizeof y);	memset(rank,255,sizeof rank);	for (int i=0;i
=0;--i) sa[--ws[rank[i]]]=i;

前面三行赋初值。

这是处理最开始的rankrankranksasasa,也就是还没有合并时。
wswsws数组是一个桶,用于辅助基数排序。
注意第三行rank[i]=s[i]。我们在实践的时候一开始不需要将真正的排名弄出来,我们只需知道它们的相对大小。而sis_isi作为单个字符,是可以表示它们的相对大小的。
其它就没什么了,要理解好一维的基数排序

for (int i=1,p=1;p
<<=1,m=p)

iii表示的是对于一个位置jjj,在这一轮中要用jjjj+ij+ij+i合并。

ppp表示不同的字符串的个数,初值设为111是为了循环条件p&lt;np&lt;np<n,显然1≥n1\geq n1n时就没必要做了。
为什么循环条件是p&lt;np&lt;np<n呢?因为我们发现,最后的rankrankrank数组一定是一个范围在[0,n)[0,n)[0,n)的排列
所以ppp顶多为nnn,想想,p=np=np=n时,那么其实已经排好序了,没必要再做下去,比如,可以看看上面那张图,可以发现最后一轮是没有必要的。
mmm表示的也是不同字符串的个数,只是因为在下面ppp要被用作计数器罢了。

p=0;		for (int j=n-i;j
=i) y[p++]=sa[j]-i;

当初我看得最久的是这一段……

这其实是一个小优化。
yyy是一个临时的rankrankrank数组。
在合并后,其实第二关键字可以通过上一次的sasasa数组求出
先看看二、三行。显然,[n−i,n)[n-i,n)[ni,n)这段区间内,如果要和后面的合并,只能000,应该说是补−1-11,因为rankrankrank数组在这个程序中的取值是[0,n)[0,n)[0,n)
−1-11一定是最小的,所以先把它们排在前面。
然后看倒数三行,这个就比较难理解了。
对于位置jjj,在这一轮中会对j−ij-iji有影响,所以说,j−i≥0j-i \geq 0ji0
因为sasasa是有序的,所以我们顺序枚举sajsa_jsaj,将其中满足以上条件的加入yyy中。

for (int j=0;j
=0;--j) sa[--ws[wv[j]]]=y[j];

这一段的作用就是以第一关键字来进行一次基数排序,和上面的那个一样的道理。

swap(rank,y);		p=1;		rank[sa[0]]=0;		for (int j=1;j

ppp起到计数器的作用。

重点是最后一行y[sa[j-1]]==y[sa[j]] && y[sa[j-1]+i]==y[sa[j]+i]
这是在比较saj−1sa_{j-1}saj1sajsa_jsaj是否相等。如果相等,那么rankrankrank值应该要一样(不过注意,到最后时rankrankrank值一定是不同的!)
网上的标这样比较,就不怕爆掉吗?对此,我很不理解,只是开了两倍的数组来解决这个问题。


关于LCP

一些概念

LCP(i,j)LCP(i,j)LCP(i,j)表示suffix(i)suffix(i)suffix(i)suffix(j)suffix(j)suffix(j)的公共最长前缀。

height(i)=LCP(SA[i−1],SA[i])height(i)=LCP(SA[i-1],SA[i])height(i)=LCP(SA[i1],SA[i])
很明显,若ranki&lt;rankjrank_i&lt;rank_jranki<rankj,则
LCP(i,j)=min⁡k∈(ranki,rankj]heightkLCP(i,j)=\min_{k\in \left(rank_i,rank_j \right]}{height_k}LCP(i,j)=k(ranki,rankj]minheightk

如何求heightheightheight?

首先,我们要知道一个性质:

hi=heightrankih_i=height_{rank_i}hi=heightranki,也就是suffix(i)suffix(i)suffix(i)与它前一名的最长公共前缀。
那么hi≥hi−1−1h_i\geq h_{i-1}-1hihi11
证明:
suffix(k)suffix(k)suffix(k)表示suffix(i−1)suffix(i-1)suffix(i1)前一名的后缀,hi−1h_{i-1}hi1即是它们的最长公共前缀。
hi−1≤1h_{i-1} \leq 1hi11时,显然等式成立。
hi−1&gt;1h_{i-1} &gt;1hi1>1时,可以发现suffix(k+1)suffix(k+1)suffix(k+1)suffix(i)suffix(i)suffix(i)的公共后缀至少为hi−1−1h_{i-1}-1hi11。可以画张图理解一下。
所以,综上,hi≥hi−1−1h_i\geq h_{i-1}-1hihi11
利用这个性质,我们可以在O(N)O(N)O(N)的时间内求出heightheightheight数组

代码

void getheight(char s[],int rank[],int sa[],int height[]){
for (int i=0,k=0;i

其实不必真正地构出个hhh数组。


其它

建议数组从111开始,或者将rankrankrank及辅助数组yyy初值设为−1-11,因为在比较时,后面的要补000(或−1-11),我就因为这样调了很久……(被罗穗骞大佬的论文坑了)

转载于:https://www.cnblogs.com/jz-597/p/11145287.html

你可能感兴趣的文章
《Python学习之路 -- Python基础之迭代器及for循环工作原理》
查看>>
struts2注解方式的验证
查看>>
CS 和 BS 的区别和优缺点
查看>>
(三)配置本地YUM源
查看>>
【LeetCode & 剑指offer刷题】数组题17:Increasing Triplet Subsequence
查看>>
【MySQL】ERROR 1045 (28000): Access denied for user的解决方法
查看>>
centos安装mysql57
查看>>
HDU 2002 计算球体积
查看>>
GROUP BY 与聚合函数 使用注意点
查看>>
oracle表名、字段名大小写问题。
查看>>
SVN学习--VisualSVN Server和TortoiseSVN的配置和使用
查看>>
CSS-继承和层叠
查看>>
「雕爷学编程」Arduino动手做(13)——触摸开关模块
查看>>
【u119】中位数
查看>>
【42.86%】【codeforces 742D】Arpa's weak amphitheater and Mehrdad's valuable Hoses
查看>>
Python Pandas分组聚合
查看>>
Thymeleaf 学习笔记
查看>>
MAC IP等相关
查看>>
Unable to instantiate prefab. Prefab may be broken.(Unity2018.2.2报错)
查看>>
Java中的TreeMap、Comparable、Comparator
查看>>