后缀数组(SA)学习笔记

后缀数组

以下,我们将字符串的元素从 \(1\) 开始标号。后缀 \(i\) 表示以 \(i\) 开头的后缀。

定义

\(sa_i\) 表示将所有后缀按字典序排序后,第 \(i\) 小后缀的标号。

\(rk_i\) 表示后缀 \(i\) 的排名。

后缀排序

两只 log 的做法

我们采用倍增法,枚举二的幂次 \(w\)。如果对每个 \(i\) 求出 \(s[i\dots i+w-1]\) 的排名 \(rk_i\),那么发现 \(s[i\dots i+2w-1]\) 的排名就是以 \(rk_i\) 为第一关键字,\(rk_{i+w}\) 为第二关键字排序后的排名。

那么我们每新的一轮用上一轮更新好的 \(rk\)\(sa\) 排序,然后再根据 \(sa\)\(rk\) 重新标号。

如果对 \(sa\) 的排序采用 std::sort 就可以做到 \(O(n\log ^2n)\),实现如下。

P3809【模板】后缀排序

这里有一个小优化,当一轮已经出现 \(n\) 个排名时就可以不用做了(即 if(p==n) break;

#include<bits/stdc++.h>
using namespace std;
int n;string s;
const int N=1e6+5;
int sa[N],rk[N<<1],lrk[N<<1];
int w;
int main(){
//	freopen("in.in","r",stdin);
//	freopen("out.out","w",stdout);
	std::ios::sync_with_stdio(0);
	s=" ";string tmp;
	cin>>tmp;n=tmp.size();s+=tmp;
	for(int i=1;i<=n;i++){
		sa[i]=i,rk[i]=s[i];
	}
	for(w=1;w<n;w<<=1){
		sort(sa+1,sa+1+n,
		[](const int &x,const int &y)->bool{
			if(rk[x]==rk[y])return rk[x+w]<rk[y+w];
			return rk[x]<rk[y];
		});
		memcpy(lrk,rk,sizeof rk);
		int p=0;
		for(int i=1;i<=n;i++){
			if(i>1&&lrk[sa[i-1]]==lrk[sa[i]]&&lrk[sa[i-1]+w]==lrk[sa[i]+w]){
				rk[sa[i]]=p;
			}
			else rk[sa[i]]=++p;
		}
		if(p==n)break;
	}
	for(int i=1;i<=n;i++)cout<<sa[i]<<" ";
}

一只 log 的做法

由于 \(sa\) 排序时比较的是排名 \(rk\),而排名的值域是 \(O(n)\) 的,于是使用基数排序就可以做到 \(O(n\log n)\)

其中 \(p\) 代表当前排名的值域,显然第一次的值域为字符集大小。

const int N=1e6+5;
char a[N];
int n,sa[N],lsa[N],rk[N*2],lrk[N*2];
int sm[N];
signed main(){
	read(a+1), n=strlen(a+1);
	fo(i,1,n) sa[i]=i,rk[i]=a[i];
	if(n==1) rk[1]=1;
    for(int l=1,t=0,p=128;l<n;l<<=1,p=t,t=0) {
		memcpy(lsa,sa,sizeof sa);
		fo(i,0,p) sm[i]=0;
		fo(i,1,n) sm[rk[sa[i]+l]]++;
		fo(i,1,p) sm[i]+=sm[i-1];
		fd(i,n,1) sa[sm[rk[lsa[i]+l]]--]=lsa[i];
		memcpy(lsa,sa,sizeof sa);
		fo(i,0,p) sm[i]=0;
		fo(i,1,n) sm[rk[sa[i]]]++;
		fo(i,1,p) sm[i]+=sm[i-1];
		fd(i,n,1) sa[sm[rk[lsa[i]]]--]=lsa[i];
		memcpy(lrk,rk,sizeof rk);
		fo(i,1,n) 
			if(lrk[sa[i-1]]==lrk[sa[i]]&&lrk[sa[i-1]+l]==lrk[sa[i]+l])
				rk[sa[i]]=t;
			else 
				rk[sa[i]]=++t;
        if(t==n) break;
	}
	fo(i,1,n) write(sa[i],' ');
	return 0;
}

height 数组

LCP

\(lcp(i,j)\) 为后缀 \(i\) 和后缀 \(j\) 的最长公共前缀长度。

height 数组

定义数组 \(hei_i=lcp(sa_i,sa_{i-1})\)\(hei_1=0\)

O(n) 求 height 数组

引理:\(hei_{rk_i}\ge hei_{rk_{i-1}}-1\)

暴力实现即可。因为 \(hei\) 不超过 \(n\),最多减 \(n\) 次,所以最多加 \(2n\) 次,复杂度 \(O(n)\)

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

求两子串的 LCP

根据 LCP 的传递性,对于 \(i<j\),有 \(lcp(sa_i,sa_j)=\min(hei_{i+1},\dots,hei_{j})\)

于是转化为了 RMQ 问题。

本质不同子串数

子串就是后缀的前缀,计算每个后缀有多少个前缀,再减掉重复的即可。

\(sa\) 枚举后缀,那么每个后缀新增的前缀就是其所有前缀减去与上一个后缀的 LCP。所以本质不同子串数等于下列式子:

\[\frac {n(n+1)}{2}-\sum _{i=2}^n hei_i \]

检查原串中一个子串的出现次数

APIO2014回文串

求出 \(hei\) 数组后,如果要查询子串 \([l,r]\) 的出现次数,那么从 \(rk_l\) 开始在 \(hei\) 上往左右二分,使得 \(lcp\ge r-l+1\),那么分别二分出左右端点 \(L,R\),此时出现次数即 \(R-L+1\)。查询 \(hei\) 的最值使用 ST 表,则单次查询是 \(O(\log n)\)

posted @ 2025-04-17 16:25  dengchengyu  阅读(22)  评论(0)    收藏  举报