后缀数组 从零基础到入门

上一次用这样的标题还是在上一次

Part0 定义

给定一个长度为 n n n 的字符串 s s s,将其 n n n 个后缀按字典序排序(本文均为从小到大),排序后可以得到两个数组:

  • s a [ i ] sa[i] sa[i] 表示排名为 i i i 的后缀的起始位置
  • r k [ i ] rk[i] rk[i] 表示起始位置为 i i i 的后缀的排名

这就是后缀数组的组成部分,一定要熟记这两个定义qwq。

根据定义,得到两个性质:

  • s a [ r k [ i ] ] = i sa[rk[i]]=i sa[rk[i]]=i,即排名为(起始位置为 i i i 的后缀的排名)的后缀的起始位置是 i i i (不打括号就太拗口了
  • r k [ s a [ i ] ] = i rk[sa[i]]=i rk[sa[i]]=i,即以(排名为 i i i 的后缀的起始位置)为起始位置的后缀排名是 i i i

Part1 思想原理

n 2 log ⁡ n n^2\log n n2logn

STL大法,string比较 O ( n ) O(n) O(n) + sort快排 O ( n l o g n ) O(nlogn) O(nlogn)

n log ⁡ 2 n n\log^2n nlog2n

考虑倍增,当对字符串 s 1 s1 s1 s 2 s2 s2 进行比较的时候,将两个字符串分别从中间断开,得到 s 1 = l 1 + r 1 s1=l1+r1 s1=l1+r1, s 2 = l 2 + r 2 s2=l2+r2 s2=l2+r2,那么 l 1 ≠ l 2 l1 \neq l2 l1=l2 时直接根据 l 1 l1 l1 l 2 l2 l2 大小关系比较, l 1 = l 2 l1 = l2 l1=l2 时再根据 r 1 r1 r1 r 2 r2 r2 进行比较即可。

将这个倍增的思路应用到求后缀数组的过程中,具体地,对于字符串"aabaaaab",有:

(图片来源OI wiki qwq

这样的话每次比较的字符串数都翻倍,但永远比较的是二元组 ( l , r ) (l,r) (l,r),最多需要 O ( log ⁡ n ) O(\log n) O(logn) 次排序,内部用快排实现就可以很容易把时间复杂度降为 O ( n log ⁡ 2 n ) O(n\log^2n) O(nlog2n),有了很大的进步,但是仍然不够优秀。

n log ⁡ n n\log n nlogn

不难发现以上算法的复杂度瓶颈在排序,由于字符种类数有限,可以使用基数排序把排序复杂度降为 O ( n ) O(n) O(n),那么整个时间复杂度就变为 O ( n log ⁡ n ) O(n\log n) O(nlogn)

其实理解了那张图也就能明白原理,下面就只要写出代码就好了。

Part2 代码实现

由上可知,排序比较的是一个二元组,假设当前比较到 i i i,倍增长度为 k k k。那么二元组的第一关键字就是 r k [ i ] rk[i] rk[i],第二关键字就是 r k [ i + k ] rk[i+k] rk[i+k]

根据基数排序的思想,代码实现就是先对第二关键字排序,再对第一关键字排序,再更新数组 r k rk rk 的过程。

代码一定要结合 s a sa sa r k rk rk 的定义理解,可以解决很多问题qwq

Code:
int tong[N],rk[N],pre[N],sa[N],id[N],px[N];
inline bool cmp(int x,int y,int k){
	//用函数来写,同样可以减少不连续的内存访问,优化常数 
	return pre[x]==pre[y]&&pre[x+k]==pre[y+k];
	//两个串相同即第一关键字和第二关键字都相同 
}
inline void SA(){
	//初始先设定一个m,为字符串的值域
	//以后的每一个循环都要分清n和m啊qwq
	
	//先处理单个字符,开桶进行计数排序,初始化它们的排名 
	ff(i,1,n) tong[rk[i]=s[i]]++;
	ff(i,1,m) tong[i]+=tong[i-1];
	//求前缀和得出每个关键字最大名次 
	for(int i=n;i>=1;--i) sa[tong[rk[i]]--]=i;
	//没有第二关键字,正序倒序均可 
	for(int k=1;k<=n;k<<=1){
		
		//排序第二关键字,id[i]表示第二关键字排名为i的数第一关键字的位置 
		int cnt=0;
		ff(i,n-k+1,n) id[++cnt]=i;
		//i+k>n,没有第二关键字,排名最靠前 
		ff(i,1,n) if(sa[i]>k) id[++cnt]=sa[i]-k;
		//顺序枚举sa,保证排名从小到大,加入可以作为第二关键字(sa[i]>k)的点
		
		//计数排序第一关键字 
		ff(i,1,m) tong[i]=0;
		ff(i,1,n) tong[px[i]=rk[id[i]]]++;
		//用px[i]记录id[i]的排名,保证内存连续访问,可以卡常 
		ff(i,1,m) tong[i]+=tong[i-1];
		//做前缀和,使桶变为排名 
		for(int i=n;i>=1;--i) sa[tong[px[i]]--]=id[i];
		//一定要倒序
		//因为tong[px[i]]展开就是tong[rk[id[i]]]
		//这个桶里存了若干个第一关键字相同的串,要根据第二关键字排序
		//所以倒着枚举i可以使第二关键字排名递减,先取出来的就是最大的,就能求出正确的排名 
		
		//更新rk 
		ff(i,1,n) pre[i]=rk[i];
		//用pre存一下原来的rk数组,用于比较是否相同
		cnt=0;
		ff(i,1,n) rk[sa[i]]=(cmp(sa[i],sa[i-1],k)?cnt:++cnt);
		//sa已经在排序第一关键字的时候更新了,直接顺序枚举即可
		//用cnt来记录当前不同的后缀个数 
		if(cnt==n) return;
		//已经有n个不同的后缀了,无需再排序 
		m=cnt;
		//缩小值域,卡常 
	}
}

Part3 一些应用

求LCP

后缀数组还包括一个数组 h e i g h t height height(以下简称 h h h), h [ i ] h[i] h[i] 表示 s a [ i − 1 ] sa[i-1] sa[i1] s a [ i ] sa[i] sa[i] 的最长公共前缀(即LCP)的长度。

怎么用 h h h 数组求出任意两个后缀的最长公共前缀呢?假设当前求后缀 i i i j j j 的最长公共前缀,设 r k [ i [ < r k [ j ] rk[i[<rk[j] rk[i[<rk[j],那么有 ∀ k ∈ [ i , j ] \forall k\in[i,j] k[i,j] l c p ( i , j ) = min ⁡ ( l c p ( i , k ) , l c p ( k , j ) ) lcp(i,j)=\min(lcp(i,k),lcp(k,j)) lcp(i,j)=min(lcp(i,k),lcp(k,j))

证明: min ⁡ ( l c p ( i , k ) , l c p ( k , j ) ) \min(lcp(i,k),lcp(k,j)) min(lcp(i,k),lcp(k,j)) k k k i i i 的LCP与 k k k j j j 的LCP的交集,就一定是 i i i j j j 的公共前缀,故 l c p ( i , j ) > = m i n ( l c p ( i , k ) , l c p ( k , j ) ) lcp(i,j)>=min(lcp(i,k),lcp(k,j)) lcp(i,j)>=min(lcp(i,k),lcp(k,j));而由于后缀已经排好序,不可能 i i i 到变 k k k 之后 k k k j j j 又变回来,故 l c p ( i , j ) < = m i n ( l c p ( i , k ) , l c p ( k , j ) ) lcp(i,j)<=min(lcp(i,k),lcp(k,j)) lcp(i,j)<=min(lcp(i,k),lcp(k,j)),得证。

根据定义计算 h h h 数组的时间复杂度是 O ( n 2 ) O(n^2) O(n2),显然不可取。但是我们可以根据这样一个定理在 O ( n ) O(n) O(n) 的时间完成计算: h [ r k [ i ] ] > = h [ r k [ i − 1 ] ] − 1 h[rk[i]]>=h[rk[i-1]]-1 h[rk[i]]>=h[rk[i1]]1

证明:感谢 Maverik 告诉我证明挂了,所以我选择背下来!

于是我们就可以写出求 h h h 数组的代码,再用ST表处理就可以 O ( 1 ) O(1) O(1) 查询任意两个后缀的LCP了。

inline void geth(){
	for(int i=1,k=0;i<=n;++i){
		if(!rk[i]) continue; 
		if(k) --k;
		while(s[i+k]==s[sa[rk[i]-1]+k]) ++k;
		h[rk[i]]=k;
	}
}

(或许)未完待续。

  • 6
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值