后缀数组(SA)学习笔记

后缀数组

问题引入

给你一个长度为 \(n\) 的字符串 \(s\),将它的所有后缀按字典序排序。

解法

后缀数组主要关系到两个数组:\(sa\)\(rk\)

其中,\(sa[i]\) 表示将所有后缀排序后第 \(i\) 小的后缀的编号,也是所说的后缀数组,也称编号数组;

\(rk[i]\) 表示后缀 \(i\) 的排名,也称排名数组。

这两个数组满足性质:\(sa[rk[i]]=rk[sa[i]]=i\)

\(O(n^2\log n)\) 做法

直接存储每一个后缀 sort 即可。

\(O(n\log^2n)\) 做法

考虑倍增,每次按照相邻 \(2\) 个长度为 \(w=2^k\) 的子串排名作为排序的第一第二关键字对编号数组排序,越界部分排名视为 \(0\)。于是 \(\lfloor\log_2(n-1)\rfloor+1\) 次排序,单次复杂度 \(O(n\log n)\),总时间复杂度 \(O(n\log^2n)\)

\(O(n\log n)\) 做法

使用计数排序优化即可。具体实现细节见代码。

char s[N];
int n,m,p,rk[N],oldrk[N*2],sa[N],id[N],cnt[N];
void main(){
	scanf("%s",s+1);
	n=strlen(s+1);
	m=128; // 初始字符集
	for(int i=1;i<=n;i++)cnt[rk[i]=s[i]]++; // 单个字符计数排序
	for(int i=1;i<=m;i++)cnt[i]+=cnt[i-1];
	for(int i=n;i>=1;i--)sa[cnt[rk[i]]--]=i; // 倒序循环,第一关键字相同时索引大的排在后面
	for(int w=1;;w<<=1,m=p){
		int cur=0;
		for(int i=n-w+1;i<=n;i++)id[++cur]=i; // 第二关键字无需计数排序
		for(int i=1;i<=n;i++) // 其实就是把超出字符串范围的 sa[i] 放到 sa 数组头部
			if(sa[i]>w)id[++cur]=sa[i]-w; // 然后把剩下的依原顺序放入
		for(int i=0;i<=m;i++)cnt[i]=0;
		for(int i=1;i<=n;i++)cnt[rk[i]]++;
		for(int i=1;i<=m;i++)cnt[i]+=cnt[i-1];
		for(int i=n;i>=1;i--)sa[cnt[rk[id[i]]]--]=id[i]; // 倒序循环,第一关键字相同时在 id 中靠后的元素排在后面
		p=0;
		for(int i=1;i<=n;i++)oldrk[i]=rk[i];
		for(int i=1;i<=n;i++)
			if(oldrk[sa[i]]==oldrk[sa[i-1]]&&oldrk[sa[i]+w]==oldrk[sa[i-1]+w])
				rk[sa[i]]=p;
			else
				rk[sa[i]]=++p;
		if(p==n)break;
	}
	for(int i=1;i<=n;i++)writeu(sa[i]),SPA;
}

应用

寻找最小的循环移动位置

复制一遍后跑 SA 即可。例:P4051 [JSOI2007] 字符加密

在字符串中找子串

在线地在字符串 \(S\) 中寻找模式串 \(T\)。子串必然是后缀的前缀,只需要预先跑出 SA 二分就可以做到 \(O(|T|\log|S|)\) 的复杂度。

height 数组

定义:\(lcp(i,j)\) 表示后缀 \(i\) 与后缀 \(j\) 的最长公共前缀长度。

\(height[i]=lcp(sa[i],sa[i-1])\),即第 \(i\) 名的后缀与它前一名的后缀的最长公共前缀。\(height[1]\) 可以视作 \(0\)

解法

我们有 \(height[rk[i]]\ge height[rk[i-1]]-1\),因此暴力求即可,复杂度是 \(O(n)\) 的。

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

应用

两子串最长公共前缀

我们有 \(lcp(sa[i],sa[j])=\min\{height[i+1..j]\}\),RMQ 即可。

比较一个字符串的两个子串的大小关系

假设需要比较的是 \(A=S[a..b]\)\(B=S[c..d]\) 的大小关系。

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

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

不同子串的数目

即全部子串减去重复统计的“后缀的前缀”,答案即为 \(\frac{n(n+1)}2-\sum_{i=2}^nheight[i]\)

posted @ 2026-07-07 21:31  leo120306  阅读(1)  评论(0)    收藏  举报