SA总结

后缀数组。要实现的是后缀排序,就是把 \(s\) 的所有后缀排序。包含两个数组(或者可以视作映射):\(sa_i\),第 \(i\) 小的后缀在原串中的开始位置;\(rk_i\),在原串中开始位置为 \(i\) 的后缀(以下称之为后缀 \(i\))的排名。这两个互为反函数,复合一下就消了:\(sa_{rk_i}=rk_{sa_i}=i\)

一般来说都用倍增求 SA,\(O(n\log n)\),在 OI 中已经够用了。有 \(O(n)\) 的求法,但不好写,所以一般不会卡 \(O(n\log n)\) 的写法。

于是学习倍增求 SA 就好。

倍增求 SA

考虑依次比较后缀中的第 \(1,2,3\dots 2^k\) 位。

首先按第一个字符排序,即对原串中的每个字符排序。

然后开始倍增,考虑对 \(k=n\) 排序之后,对 \(k=2n\) 时排序。将每个子串拆成前 \(2^{k-1}\) 位和后 \(2^{k-1}\) 位,于是就是双关键字排序,使用基数排序即可一次 \(O(n)\) 排序,再加上倍增就是 \(O(n\log n)\)

具体说一下基数排序的过程。

这里 \(rk_i\) 尚有重复值,于是可以视作每个后缀的权值。

\(m\) 表示 \(rk\) 的值域。

首先根据 \(rk\) 求出初始的 \(sa\)\(buc_i\) 表示 \(rk=i\) 的后缀的个数,做前缀和之后就表示 \(rk\le i\) 的后缀的个数。然后考虑往 \(sa\) 中填数,相同权值的后缀我们不关心其内部排名,于是直接令位置靠后的排名较大即可。

然后开始倍增,枚举 \(w\),每轮倍增之后更新一下 \(rk\) 的值域,是一点常数优化。

现在来看每一轮的过程。

首先求出 \(id_i\) 表示第二关键字排名为 \(i\) 的后缀的开始位置。显然开始位置在 \([n-w+1,n]\) 之间的后缀,其第二关键字为空,肯定靠前,于是直接加入。对于其余后缀,直接枚举第二关键字的排名 \(i\),然后 \(sa_i\) 就得到了第二关键字的起始位置,再 \(sa_i-w\) 就是整个的起始位置。

然后再进行基数排序,还是按 \(rk\) 放到桶里。这次权值相同的不能随便排序,而是要按第二关键字的大小排序。于是我们从大到小枚举第二关键字,这样先枚举到的第二关键字更加靠后,于是给这个子串更大的排名。一开始把 \(rk\) 扔到桶里已经保证了第一关键字是有序的,于是不用管了。

接下来 \(sa\) 已经求好了,我们要利用 \(sa\) 重新求出 \(rk\)。我们会用到之前的 \(rk\),于是可以用 \(id\)\(rk\) 保存下来。

首先对于 \(sa_1\),根据反函数的性质,\(rk_{sa_1}=1\),并且利用 \(p\) 来记录 \(rk\) 当前的值域,现在 \(p=1\)

然后从 \(2\) 开始,扫各个 \(sa\)。如果后缀 \(sa_i\) 与后缀 \(sa_{i-1}\) 的权值相同,并且后缀 \(sa_i+w\) 与后缀 \(sa_{i-1}+w\) 的权值相同,那么现在这两个后缀的权值就相等,\(rk_{sa_i}=rk_{sa_{i-1}}=p\)。否则就不相等,那么 \(sa_i\) 的权值比 \(sa_{i-1}\) 的权值大 \(1\),所以 \(rk_{sa_i}=rk_{sa_{i-1}}+1=p+1\),然后 \(p\gets p+1\)

算好 \(rk\) 后,若值域为 \(n\),那么每个位置的 \(rk\) 互不相同,就可以结束了。

注意到判断中 \(sa_i+w\) 的值域其实是 \(2n\) 的,但是 \(rk\)\(id\) 只用开到 \(n+1\)。因为若 \(sa_i\)\(sa_{i-1}\) 的权值不同,就不会进后面那个判断,若 \(sa_i\)\(sa_{i-1}\) 的权值相同,则 \([sa_i,sa_i+w-1]\)\([sa_{i-1},sa_{i-1}+w-1]\) 相同,那么 \(sa_i+w\le n+1\)\(sa_{i-1}+w\le n+1\)

#include<bits/stdc++.h>

using namespace std;

const int maxn=1e6+10;
int n,m,p,rk[maxn],sa[maxn],id[maxn],buc[maxn];
char s[maxn];

void build(){
	n=strlen(s+1);
	m=128,p=0;
	for(int i=1;i<=n;++i) buc[rk[i]=s[i]]++;
	for(int i=1;i<=m;++i) buc[i]+=buc[i-1];
	for(int i=n;i;--i) sa[buc[rk[i]]--]=i;
	for(int w=1;;w<<=1,m=p,p=0){
		for(int i=n-w+1;i<=n;++i) id[++p]=i;
		for(int i=1;i<=n;++i) if(sa[i]>w) id[++p]=sa[i]-w;
		for(int i=1;i<=m;++i) buc[i]=0;
		for(int i=1;i<=n;++i) buc[rk[i]]++;
		for(int i=1;i<=m;++i) buc[i]+=buc[i-1];
		for(int i=n;i;--i) sa[buc[rk[id[i]]]--]=id[i],id[i]=rk[i];
		rk[sa[1]]=1,p=1;
		for(int i=2;i<=n;++i) rk[sa[i]]=(id[sa[i]]==id[sa[i-1]]&&id[sa[i]+w]==id[sa[i-1]+w])?p:++p;
		if(p==n) break;
	}
	return;
}

int main(){
	scanf("%s",s+1);
	build();
	for(int i=1;i<=n;++i) printf("%d ",sa[i]);
	return 0;
} 

SA 与 LCP

定义 \(LCP(i,j)\) 表示后缀 \(sa_i\)\(sa_j\) 的最长公共前缀的长度。

首先,有显然的式子 \(LCP(i,j)=LCP(j,i)\) 以及 \(LCP(i,i)=n-sa_i+1\)

然后就是不那么显然的式子:\(\forall 1\le i\le k\le j\le n,LCP(i,j)=\min\Big \{LCP(i,k),LCP(k,j)\Big \}\)

证明一下:设 \(t=\min\Big\{LCP(i,k),LCP(k,j)\Big\}\),那么 \(LCP(i,k)\ge t\)\(LCP(k,j)\ge t\)。后缀 \(sa_i\) 的前 \(t\) 位与 \(sa_k\) 的前 \(t\) 位相同,\(sa_k\) 的前 \(t\) 位与 \(sa_j\) 的前 \(t\) 位相同,于是 \(sa_i\) 的前 \(t\) 位与 \(sa_j\) 的前 \(t\) 位相同。所以 \(LCP(i,j)\ge t\)

假设 \(LCP(i,j)=r>t\),那么后缀 \(sa_i\) 的前 \(r\) 位与 \(sa_j\) 的前 \(r\) 位相同,于是 \(sa_i\) 的前 \(t+1\) 位与 \(sa_j\) 的前 \(t+1\) 位也必定相同。由 \(t\) 的定义以及 \(i\le k\le j\),可以得到后缀 \(sa_i\) 的第 \(t+1\) 位不大于 \(sa_k\) 的第 \(t+1\) 位,且 \(sa_j\) 的第 \(t+1\) 位不小于 \(sa_k\) 的第 \(t+1\) 位,且两边不能同时取得等号,所以 \(sa_i\) 的第 \(t+1\) 位不等于 \(sa_j\) 的第 \(t+1\) 位,与假设矛盾,所以原命题得证。

再来一个不那么显然的式子:\(LCP(i,j)=\min\limits_{i<k\le j}\Big\{LCP(k,k-1)\Big\}\)

证明一下,通过上一个式子可知 \(LCP(i,j)=\min\Big\{LCP(i+1,i),LCP(i+1,j)\Big\}\),然后继续拆后面那一项即可证明。或许可以用数归写成更严谨的形式。总之证完了。

\(height_i=LCP(i,i-1)\),并且 \(height_1=0\),那么求出 \(height\) 之后 \(LCP\) 就是区间求 \(\min\)

再令 \(h_i=height_{rk_i}\),那么 \(height_i=h_{sa_i}\)

最后一个引理:\(h_i\ge h_{i-1}-1\)

证明一下,先把两边写开,\(height_{rk_i}\ge height_{rk_{i-1}}-1\),也即 \(LCP(rk_i,rk_i-1)\ge LCP(rk_{i-1},rk_{i-1}-1)-1\)。设 \(x=sa_{rk_i-1},y=sa_{rk_{i-1}-1}\)。那么就是要证 \(LCP(rk_i,rk_x)\ge LCP(rk_{i-1},rk_y)-1\)。如果后缀 \(i-1\)\(y\) 的首字母不同,那么显然成立。如果首字母相同,那么去掉首字母后可以得到后缀 \(y+1\)\(i\),由于 \(rk_y<rk_{i-1}\),故而 \(rk_{y+1}<rk_i\),并且 \(LCP(rk_{y+1},rk_i)=LCP(rk_{i-1},rk_y)-1\)。又 \(rk_x=rk_i-1\),所以 \(\forall rk_k<rk_i,LCP(rk_i,rk_x)\ge LCP(rk_i,rk_k)\)。那么综上,\(LCP(rk_i,rk_x)\ge LCP(rk_i,rk_{y+1})=LCP(rk_{i-1},rk_y)-1\),即原命题成立,\(h_i\ge h_{i-1}-1\)

于是求 \(h_i\) 时,最多进行 \(n\)\(-1\),于是最多往后扫 \(2n\) 次,所以是 \(O(n)\) 的。

以上三个式子是 SA 中很重要的部分,还请熟记。

应用

求两个后缀的 LCP

直接求出 \(height\),然后应用第二个式子(不妨 \(rk_i<rk_j\)):\(LCP(rk_i,rk_j)=\min\limits_{rk_i<k\le rk_j}\Big\{height_k\Big\}\),就是 RMQ,随便做。

比较两个子串的大小

\(A=s_{a\dots b},B=s_{c\dots d}\)

先求出 \(L=LCP(rk_a,rk_c)\),若 \(L=\min(|A|,|B|)\),则 \(A<B\iff |A|<|B|\)

否则 \(A<B\iff rk_a<rk_c\)

本质不同子串个数

可以考虑用所有子串数目 \(\frac{n(n+1)}{2}\) 减去重复的。

子串可以视作后缀的前缀,于是按 \(rk\) 递增的顺序枚举每个后缀,那么对于当前后缀,新增的部分就是整个长度减去与上一个前缀的 \(LCP\) 的差。于是重复的部分就是 \(\sum\limits_{2\le k\le n} height_k\),答案就是 \(\frac{n(n+1)}{2}-\sum\limits_{2\le k\le n} height_k\)

其实用增量的想法也可以导出答案,就是每次新增的量加在一起,于是答案为 \(\frac{n(n+1)}{2}-\sum\limits_{1\le k\le n} height_k\),而 \(height_1=0\)

其他待补

posted @ 2025-04-18 18:51  RandomShuffle  阅读(39)  评论(0)    收藏  举报