所谓后缀数组,就是存储后缀的数组
(逃)
前言
为什么一个算法,如此难以理解却依然是成为一个成熟OIer不可回避的必修课?
足以可见后缀家族功能的强大
首先,由于其本身的性质,后缀数组对字典序相关的问题十分擅长
同时,由于
h
e
i
g
h
t
height
height 数组的众多优秀性质,它在处理公共串问题和 LCP 问题上也十分强大
(我目前SA的题加起来也没做上十道,所以这样的“总结”请选择性阅读)
解析
后缀排序
给出一个字符串,把所有后缀按照字典序排序
n ≤ 1 0 6 n\le10^6 n≤106
考虑倍增
一开始子串长度为
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.2s→0.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} sai−1 的最长公共前缀( l c p ( s a i , s a i − 1 ) lcp(sa_i,sa_{i-1}) lcp(sai,sai−1))。特别的, 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 i≤k≤j 均成立.
证明:
首先,
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 heightrki≥heightrki−1−1
证明:
r
k
i
−
1
≤
1
rk_{i-1}\le1
rki−1≤1 时,显然成立
r
k
i
−
1
>
1
rk_{i-1}>1
rki−1>1 时,设
r
k
i
−
1
−
1
=
k
rk_{i-1}-1=k
rki−1−1=k(
k
k
k 就是
i
−
1
i-1
i−1 按字典序排序后的前一个),那么:
若
i
−
1
i-1
i−1 和
k
k
k 的首字母不同,
h
i
−
1
=
0
h_{i-1}=0
hi−1=0 ,显然成立
若
i
−
1
i-1
i−1 和
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,i−1)−1=heightrki−1−1,由引理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,rki−1),lcp(i−1,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(i−1,i)≥heightrki−1−1,即
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
heightrki≥heightrki−1−1
得证。
得出这个性质后,线性求
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!