后缀数组

发布时间 2023-08-06 20:22:07作者: SError

SA

基数排序

一般采用 LSD(Least Significant Digital),从键值的最低位开始排序。

定义

\(suf(i)\) 为起始下标为 \(i\) 的后缀。

\(sa[i]\) 为排名第 \(i\) 的后缀的起始位置。

\(rk[i]\)\(suf(i)\) 的排名。


P3809 【模板】后缀排序

对于一个长为 \(n\) 的字符串,求出其后缀数组 \(\{sa_n\}\).

倍增法

  • 把每个字符当作长为 \(1\) 的后缀,按字符值进行编号。

  • 将当前长度为 \(len\) 的后缀扩展至 \(2len\),按前半、后半部分的编号进行排序。

  • 时间复杂度 \(O(n\log n)\),空间线性。

具体地,第二步中的前、后两部分会形成若干个二元组。

直接排序的复杂度为 \(O(n\log^2 n)\).

使用 LSD 进行基数排序,以第二维为第一关键字。

我们可以拿一个 \(tp\) 记录第二关键字中排名为 \(i\) 的数的位置。

#include<bits/stdc++.h>
#define N 1000010
using namespace std;
int n,m;char s[N];
int a[N],sa[N],rk[N],tp[N];
int get(char c){
	if(c>='0'&&c<='9')return c-'0'+1;
	if(c>='A'&&c<='Z')return c-'A'+11;
	return c-'a'+37;
}
void init(){
	n=strlen(s+1),m=62;
	for(int i=1;i<=n;i++)
		rk[i]=get(s[i]),tp[i]=i;
}
void LSD(){//基数排序
	for(int i=1;i<=m;i++)a[i]=0;
	for(int i=1;i<=n;i++)a[rk[i]]++;
	for(int i=1;i<=m;i++)a[i]+=a[i-1];
	for(int i=n;i;i--)sa[a[rk[tp[i]]]--]=tp[i];
}
void SA(){
	for(int w=1,p=0;w<=n;m=p,p=0,w<<=1){
		for(int i=n-w+1;i<=n;i++)tp[++p]=i;
    		//p记录最高排名,若p=n则无需排序
            	//n-w+1后面均无第二关键字,补0设成极小值排在前面
		for(int i=1;i<=n;i++)
			if(sa[i]>w)tp[++p]=sa[i]-w;
		LSD(),swap(rk,tp),p=rk[sa[1]]=1;
        	//有rk[sa[i]]=i
		for(int i=2;i<=n;i++)
			rk[sa[i]]=(tp[sa[i]]==tp[sa[i-1]]&&
				tp[sa[i]+w]==tp[sa[i-1]+w])?p:++p;
                //两后缀相等排名不变
		if(p==n)return;
	}
}
int main(){
	scanf("%s",s+1);
	init(),LSD(),SA();
	for(int i=1;i<=n;i++)
		printf("%d ",sa[i]);
	printf("\n");
	
	return 0;
}

感性理解一下吧。时间复杂度 \(O(n\log n)\)
.

  • 不要使用任何 vector.

一些应用

  • 最小循环移动位置

这个概念比较奇怪但是可以看看 P4051 [JSOI2007] 字符加密

\(S\leftarrow SS\),求一遍 SA 即可。

本题字符集包含了字符所以可以让程序里的 \(m\) 取一个大值。

main 函数:

int main(){
	scanf("%s",_s+1),_n=strlen(_s+1);
	for(int i=1;i<=_n;i++)
		s[i]=s[i+_n]=_s[i];
	init(),LSD(),SA();
	for(int i=1;i<=n;i++){
		if(sa[i]<=_n)
			printf("%c",s[sa[i]+_n-1]);
	}
	printf("\n");
	
	return 0;
}
  • 匹配子串

在线地在主串 \(T\) 里寻找模式串 \(S\).

做出 \(T\) 的 SA,若 \(S\)\(T\) 中出现,那么 \(S\)\(T\) 的某个后缀的前缀。

在排序过的后缀里二分,一次比较的时间为 \(O(|S|)\),故总时间 \(O(|S|\log|T|)\).

若求出现次数,则其在 SA 数组中相邻,同样上一次二分即可。

  • 取首尾最小化字典序

P2870 [USACO07DEC] Best Cow Line G

暴力做法即单次 \(O(n)\) 比较当前串与反串的大小。考虑优化。

一个十分方便的想法是将原串与反串拼接,以 # 隔开,这样就能够做到 \(O(n\log n)\) 预处理,单次 \(O(1)\) 的复杂度。

main 函数:

int main(){
	scanf("%d",&_n),getchar();
	for(int i=1;i<=_n;i++){
		scanf("%c",&s[i]),getchar();
		s[_n*2+1-i+1]=s[i];
	}
	s[_n+1]='#';
	init(),LSD(),SA();
	for(int i=1,l=1,r=_n;i<=_n;i++){
		printf("%c",rk[l]<rk[n+1-r]?s[l++]:s[r--]);
		if(i%80==0)printf("\n");
	}
	
	return 0;
}

height 数组

  • LCP(最长公共前缀)

这里我们记 \(lcp(i,j)\) 为后缀 \(i\)\(j\) 的 LCP(的长度)。

  • height 数组的定义

\(height[i]=lcp(sa[i],sa[i-1])\),即第 \(i\) 名的后缀与 \(i-1\) 名的后缀的 LCP。

\(height[1]\) 视作 \(0\).

  • height 数组求解

引理:\(height[rk[i]]\ge height[rk[i-1]]-1\)

证明:当 \(height[rk[i-1]]\le 1\) 时显然成立。下面对 \(height[rk[i-1]]>1\) 的情况进行讨论。

由 height 数组的定义有 \(lcp(sa[rk[i-1]],sa[rk[i-1]-1])=height[rk[i-1]]>1\).

也就是后缀 \(i-1\) 和后缀 \(sa[rk[i-1]-1]\) 有长为 \(height[rk[i-1]]\) 的 LCP。用一个字符 \(a\) 和一个串 \(A\) 表示它。

\(LCP=aA\),且 \(|A|=height[rk[i-1]]-1\).

后缀 \(i-1\) 可表示为 \(aAD\),后缀 \(sa[rk[i-1]-1]\) 可表示为 \(aAB\),且有 \(B<D\)\(D\) 非空,\(B\) 可空。

后缀 \(i\) 可表示为 \(AD\),存在后缀 \((sa[rk[i-1]-1]+1)\)\(AB\).

由于后缀 \(sa[rk[i]-1]\) 在大小排名上仅比后缀 \(sa[rk[i]]=i\) 小一位,\(AB<AD\),所以有 \(AB\le\text{后缀}sa[rk[i]-1]<AD\).

显然后缀 \(i\) 和后缀 \(sa[rk[i]-1]\) 有公共前缀 \(A\).

\(height[rk[i]]\ge height[rk[i-1]]-1\).

实在看不懂就感性理解了。

线性复杂度代码实现

根据引力暴力求解。

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++;
	height[rk[i]]=k;
}

\(k\) 恒小于等于 \(n\),最多减 \(n\) 次,加 \(2n\) 次。

故复杂度线性。


一些应用

  • 两子串 LCP

\[lcp(sa[i],sa[j])=\min\{height[i+1\sim j]\} \]

\(height\) 一直大于某个数,则前这么多位一直不变。反之,由于后缀排过序,变化的位不可能变回来。

由此两子串的 LCP 转化成了一个 RMQ 问题。

  • 比较两子串大小

记两者为 \(A=s[a\sim b]\)\(B=s[c\sim d]\).

\(lcp(a,c)\ge min(|A|,|B|)\)\(A<B\Longleftrightarrow |A|<|B|\).

否则,\(A<B\Longleftrightarrow rk[a]<rk[c]\).

  • 不同字串数目

P2408 不同子串个数

子串即后缀的前缀,枚举每个后缀,计算前缀总数再减掉重复的。

前缀总数即 \(\displaystyle\frac{n(n+1)}{2}\).

将后缀排序后枚举,新增的子串是除了与上一个后缀的 LCP 剩下的前缀。

答案是

\[\frac{n(n+1)}{2}-\sum_{i=2}^{n}height[i] \]

record

  • 出现次数至少为 \(k\) 的最长子串

P2852 [USACO06DEC] Milk Patterns G

取所有相邻的 \(k-1\)\(height\)\(\min\)\(\max\).

配合单调队列实现。

--