【学习笔记】SA/SAM

1.【SA】

参看oi-wiki

1.2【数组】

  • \(sa_i\):第 \(i\) 小的字符串的下标

  • \(rk_i\):下标为 \(i\) 的字符串的排名;是求 \(sa\) 的中间产物,并无应用

  • \(h_i\):下标为 \(i\) 的字符串与 \(i-1\) 的字符串的最长公共前缀长度

  • \(height\):排名为 \(i\) 的字符串与 \(i-1\) 的字符串最长公共前缀长度;是 SA 的精髓,也是相比于 SAM 的优势所在

1.3【基数排序正确性】

基数排序是稳定排序,故而正确

1.4【简单应用】

本质不同子串计数:减去 \(sum(height[i])\)

1.5【构建方式】

1.5.1【基础构建】

#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
char c[N];
int sa[N],rk[N*2];
int tmp[N];
int tot;
int up=131;
int n;
int bin[N];
void sortsa(){//按照第一关键字排序
	memset(bin,0,sizeof bin);
	for(int i=1;i<=n;i++) bin[rk[i]]++;
	for(int i=1;i<=up;i++) bin[i]+=bin[i-1];
	for(int i=n;i>0;i--) sa[bin[rk[tmp[i]]]--]=tmp[i];//保证稳定排序
}
int main(){
	string s;cin>>s;
	n=0;
	for(auto v:s) c[++n]=v;
	for(int i=1;i<=n;i++){
		rk[i]=c[i];
		tmp[i]=i;
	}
	sortsa();
	//rk是离散化后的关键字
	for(int i=1;i<n;i<<=1){
		tot=0;
		//tmp作sa(第二关键字排序)
		for(int j=n-i+1;j<=n;j++) tmp[++tot]=j;
		for(int j=1;j<=n;j++){
			if(sa[j]-i>0) tmp[++tot]=sa[j]-i;
		}
		sortsa();
		//以上可直接替换为sort
		//tmp作rk(重新分配)
		tmp[sa[1]]=up=1;
		
		for(int j=2;j<=n;j++){
			if(rk[sa[j]]!=rk[sa[j-1]]||rk[sa[j]+i]^rk[sa[j-1]+i]) up++;
			tmp[sa[j]]=up;
		}
		memcpy(rk,tmp,sizeof tmp);
	}
	for(int i=1;i<=n;i++) cout<<sa[i]<<" ";
	return 0;
}

1.5.2【height构建】

\(h\) 递推性质: \(h[i]≥h[i−1]−1\)

\(height\) 区间最小值定理:任意两个后缀的 \(LCP\) 等于它们排名区间内 \(height\) 数组的最小值

int height[N];
void getHeight(){
	int k=0;
	for(int i=1;i<=n;i++){
		if(rk[i]==1){
			k=height[rk[i]]=0;
			continue;
		}
		if(k) k--;
		int j=sa[rk[i]-1];
		while(i+k<=n&&j+k<=n&&c[i+k]==c[j+k]) k++;
		height[rk[i]]=k;
	}
}

1.6【优势】

SAM功能比SA丰富许多,但是SA仍有一些优势:

SA相比SAM最核心的不可替代优势是 \(LCP\)(最长公共前缀)查询。

配合 \(height\) 数组,SA能 \(O(1)\) 回答任意后缀的 \(LCP\),而SAM需要求 \(Parent\) 树的 \(LCA\),实现繁琐得多。


此外,SA还有两大绝对优势:

无视字符集:处理大字符集时内存稳定,SAM则可能内存爆炸。

天生有序:直接输出所有后缀的字典序排名,SAM做不到这一点。

而SA掌握以上内容即可,其余建议使用SAM

2.【SAM】

请移步Link

posted @ 2026-08-25 09:05  Aistyr  阅读(7)  评论(0)    收藏  举报