模板:后缀数组(SA)

所谓后缀数组,就是存储后缀的数组

(逃)

前言

为什么一个算法,如此难以理解却依然是成为一个成熟OIer不可回避的必修课?
足以可见后缀家族功能的强大

首先,由于其本身的性质,后缀数组对字典序相关的问题十分擅长
同时,由于 h e i g h t height height 数组的众多优秀性质,它在处理公共串问题和 LCP 问题上也十分强大
(我目前SA的题加起来也没做上十道,所以这样的“总结”请选择性阅读)

解析

后缀排序

P3809 【模板】后缀排序

给出一个字符串,把所有后缀按照字典序排序
n ≤ 1 0 6 n\le10^6 n106

考虑倍增
一开始子串长度为 1 1 1,每个位置的排名 r k i rk_i rki 就是自己位置的字符
然后在已知长度为 w w w 的所有子串的排名的情况下,以 r k i + w rk_{i+w} rki+w 为第二关键字, r k i rk_i rki 为第一关键字排序,可以得到长度为 2 w 2w 2w 的所有子串的排名(空串的排名视为负无穷)
每次用 sort 的话,时间复杂度 O ( n l o g 2 n ) O(nlog^2n^) O(nlog2n)

优化1:基数排序

注意到这里的排序是关于大小的排序,且值域(排名)只有 O ( n ) O(n) O(n)
所以我们可以使用基数排序代替 sort,时间复杂度变成 O ( n l o g n ) O(nlogn) O(nlogn)

注意! 基数排序重新排列的循环必须倒序枚举,这样才能保证排序的稳定性

memset(cnt,0,sizeof(cnt));
memcpy(oldrk,rk,sizeof(rk));
for(int i=1;i<=n;i++) ++cnt[rk[id[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];
p=0;
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;
}
m=p;

优化2:简化第一次排序

第一次是关于 r k i w rk_{i_w} rkiw 排序
并不需要基数排序,只需要:

p=0;
for(int i=n;i>n-w;i--) id[++p]=i;
for(int i=1;i<=n;i++){
	if(sa[i]>w) id[++p]=sa[i]-w;
}

即可

优化3:提前break

玄学优化
大概就是,不必真的倍增到总长度,只需要让所有字符串的排名互相分开即可
这东西在全是 a 这样的串中可以说是等于没有,但在不少时候优化巨大(比如本题 2.2 s → 0.8 s 2.2s\to 0.8s 2.2s0.8s)

完整代码

s a i sa_i sai:排名为 i i i 的后缀的编号
r k i rk_i rki:后缀 i i i 的排名

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
#define debug(...) fprintf(stderr,__VA_ARGS__)
const int N=1e6+100;
inline ll read(){
	ll x(0),f(1);char c=getchar();
	while(!isdigit(c)){if(c=='-')f=-1;c=getchar();}
	while(isdigit(c)){x=(x<<1)+(x<<3)+c-'0';c=getchar();}
	return x*f;
}
int n,m,k;
char s[N];
int rk[N<<1],oldrk[N<<1],id[N],sa[N],cnt[N],p;
void write(int x){
	if(x>9) write(x/10);
	putchar('0'+x%10);return;
}
signed main() {
#ifndef ONLINE_JUDGE
	//freopen("a.in","r",stdin);
	//freopen("a.out","w",stdout);
#endif
	scanf(" %s",s+1);n=strlen(s+1);
	m=122;
	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){
		p=0;
		for(int i=n;i>n-w;i--) id[++p]=i;
		for(int i=1;i<=n;i++){
			if(sa[i]>w) id[++p]=sa[i]-w;
		}
		memset(cnt,0,sizeof(cnt));
		memcpy(oldrk,rk,sizeof(rk));
		for(int i=1;i<=n;i++) ++cnt[rk[id[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];
		p=0;
		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;
		}
		m=p;
		if(m==n) break;//优化3
	}
	for(int i=1;i<=n;i++) write(sa[i]),putchar(' ');
	return 0;
}
/*

*/

LCP与height

定义:

h e i g h t ( i ) height(i) height(i) 表示后缀 s a i sa_i sai 和后缀 s a i − 1 sa_{i-1} sai1 的最长公共前缀( l c p ( s a i , s a i − 1 ) lcp(sa_i,sa_{i-1}) lcp(sai,sai1))。特别的, l c p ( 1 ) = 0 lcp(1)=0 lcp(1)=0

感性理解来说,把所有后缀按照字典序排序后, h e i g h t ( i ) height(i) height(i) 就是相邻两个后缀的相同部分的长度。

引理1: l c p ( i , j ) = min ⁡ ( l c p i , k , l c p k , j ) lcp(i,j)=\min (lcp_{i,k},lcp_{k,j}) lcp(i,j)=min(lcpi,k,lcpk,j),对于任意的 i ≤ k ≤ j i\le k\le j ikj 均成立.

证明:
首先, min ⁡ ( l c p i , k , l c p k , j ) \min (lcp_{i,k},lcp_{k,j}) min(lcpi,k,lcpk,j) k k k i , j i,j i,j 共同的公共前缀,所以也必然是 i , j i,j i,j 的公共前缀, l c p ( i , j ) ≥ min ⁡ ( l c p i , k , l c p k , j ) lcp(i,j)\ge\min (lcp_{i,k},lcp_{k,j}) lcp(i,j)min(lcpi,k,lcpk,j)
同时,由于字典序单调的性质, i i i 变到 k k k 变化的前缀在 k k k 变化到 j j j 时必然不可能再变回来,否则 j j j 的字典序就比 k k k 小了,所以有 l c p ( i , j ) ≤ min ⁡ ( l c p i , k , l c p k , j ) lcp(i,j)\le\min (lcp_{i,k},lcp_{k,j}) lcp(i,j)min(lcpi,k,lcpk,j)
综上, l c p ( i , j ) = min ⁡ ( l c p i , k , l c p k , j ) lcp(i,j)=\min (lcp_{i,k},lcp_{k,j}) lcp(i,j)=min(lcpi,k,lcpk,j),证毕。

引理2: h e i g h t r k i ≥ h e i g h t r k i − 1 − 1 height_{rk_i}\ge height_{rk_{i-1}}-1 heightrkiheightrki11

证明:
r k i − 1 ≤ 1 rk_{i-1}\le1 rki11 时,显然成立
r k i − 1 > 1 rk_{i-1}>1 rki1>1 时,设 r k i − 1 − 1 = k rk_{i-1}-1=k rki11=k k k k 就是 i − 1 i-1 i1 按字典序排序后的前一个),那么:
i − 1 i-1 i1 k k k 的首字母不同, h i − 1 = 0 h_{i-1}=0 hi1=0 ,显然成立
i − 1 i-1 i1 k k k 的首字母相同,那么考虑字符串 k + 1 k+1 k+1,由于k 去掉首字符变成 k+1,i-1 去掉首字母变成 i,所以 k + 1 k+1 k+1 也一定在 i i i 的前面,同时 l c p ( k + 1 , i ) = l c p ( k , i − 1 ) − 1 = h e i g h t r k i − 1 − 1 lcp(k+1,i)=lcp(k,i-1)-1=height_{rk{i-1}}-1 lcp(k+1,i)=lcp(k,i1)1=heightrki11,由引理1,有 l c p ( k + 1 , i ) = min ⁡ ( l c p ( k + 1 , r k i − 1 ) , l c p ( i − 1 , i ) ) lcp(k+1,i)=\min (lcp(k+1,rk_i-1),lcp(i-1,i)) lcp(k+1,i)=min(lcp(k+1,rki1),lcp(i1,i)),故 l c p ( i − 1 , i ) ≥ h e i g h t r k i − 1 − 1 lcp(i-1,i)\ge height_{rk{i-1}}-1 lcp(i1,i)heightrki11,即 h e i g h t r k i ≥ h e i g h t r k i − 1 − 1 height_{rk_i}\ge height_{rk_{i-1}}-1 heightrkiheightrki11
得证。
得出这个性质后,线性求 h e i g h t height height 的代码就不难写出了:

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

Thanks for reading!

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值