后缀数组(SA)及height数组

最近感觉自己越来越蒟蒻了……后缀数组不会,费用流不会……
看着别人切一道又一道的题,我真是很无奈啊……
然后,我花了好长时间,终于弄懂了后缀数组。

后缀数组是什么?

后缀 S A SA SA数组

给你一个字符串,让你将每个后缀排序,就是一个后缀数组
比如,字符串为ababa,就会搞出一个这样的东西:

a
aba
ababa
ba
baba
SA={4,2,0,3,1};

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

r a n k rank rank数组

相当于逆着的 S A SA SA r a n k [ s a [ i ] ] = i rank[sa[i]]=i rank[sa[i]]=i
#后缀数组怎么求?

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

打个选择排序,每次比较用 O ( N ) O(N) O(N)的方法。
当然,这样的暴力出不了奇迹。

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

仅仅是将选择排序变成快排罢了。
##方法三:倍增

倍增1.0

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

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

现在rank={1,2,1,2,1}
然后像下面的这幅图一样搞一搞。
倍增
倍增算法的精髓就在这幅图中。
枚举一个 i i i,对于每个位置 j j j,每次将 j j j j + 2 i j+2^i j+2i合并到一起(不够补0),然后排名(可以当做是离散化)。
这样就可以求出 r a n k rank rank,然后求出 S A SA SA
具体没什么好讲的,只要看懂这幅图,就懂了倍增算法的思想。
这样就可以优化到 O ( N lg ⁡ 2 N ) O(N\lg^2N) O(Nlg2N)了。

倍增2.0

然而,这不是倍增的极限。
可以用基数排序进一步地优化!!!
什么是基数排序,基数排序怎么打,请百度一下(基数排序可以用一维的数组来打,具体看代码实践)。
在此,我有意提醒的是,你可以将数看做一个m进制,在倍增合并两个数时,可以将其看做m进制的两位数,然后对它进行基数排序。
这样就有 O ( N lg ⁡ 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<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++);
	}

这就是网上通常的打法,当然,风格会有些不一样……
是不是看了后,一头雾水?
别急,慢慢解释。
首先说一下,在这个程序中, r a n k rank rank取值是在 [ 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<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;

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

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

i i i表示的是对于一个位置 j j j,在这一轮中要用 j j j j + i j+i j+i合并。
p p p表示不同的字符串的个数,初值设为 1 1 1是为了循环条件 p &lt; n p&lt;n p<n,显然 1 ≥ n 1\geq n 1n时就没必要做了。
为什么循环条件是 p &lt; n p&lt;n p<n呢?因为我们发现,最后的 r a n k rank rank数组一定是一个范围在 [ 0 , n ) [0,n) [0,n)的排列
所以 p p p顶多为 n n n,想想, p = n p=n p=n时,那么其实已经排好序了,没必要再做下去,比如,可以看看上面那张图,可以发现最后一轮是没有必要的。
m m m表示的也是不同字符串的个数,只是因为在下面 p p 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;

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

		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++);

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


关于LCP

一些概念

L C P ( i , j ) LCP(i,j) LCP(i,j)表示 s u f f i x ( i ) suffix(i) suffix(i) s u f f i x ( j ) suffix(j) suffix(j)的公共最长前缀。
h e i g h t ( i ) = L C P ( S A [ i − 1 ] , S A [ i ] ) height(i)=LCP(SA[i-1],SA[i]) height(i)=LCP(SA[i1],SA[i])
很明显,若 r a n k i &lt; r a n k j rank_i&lt;rank_j ranki<rankj,则
L C P ( i , j ) = min ⁡ k ∈ ( r a n k i , r a n k j ] h e i g h t k LCP(i,j)=\min_{k\in \left(rank_i,rank_j \right]}{height_k} LCP(i,j)=k(ranki,rankj]minheightk

如何求 h e i g h t height height?

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

代码

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;
		}
}

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


其它

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

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
YOLO高分设计资源源码,详情请查看资源内容中使用说明 YOLO高分设计资源源码,详情请查看资源内容中使用说明 YOLO高分设计资源源码,详情请查看资源内容中使用说明 YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值