0%

字符串

本文字符串下标从 11 开始,无歧义时默认 nn 为当前所述字符串长度。

后缀数组(SA)

定义 sufis[i,n]suf_i\gets s[i,n]rkirk_isufisuf_i 在所有后缀中的字典序排名,saisa_i 为字典序第 ii 小的后缀。rk,sark,sa 互逆。

例如 s=aababs=\texttt{aabab} 时,aabab<ab<abab<b<bab\texttt{aabab}<\texttt{ab}<\texttt{abab}<\texttt{b}<\texttt{bab},所以 rk=[1,3,5,2,4],sa=[1,4,2,5,3]rk=[1,3,5,2,4],sa=[1,4,2,5,3]

后缀排序指求上述数组的过程。

O(nlog2n)O(n\log^2n) 做法

考虑朴素倍增。不妨定义 rki,jrk'_{i,j} 表示将 s[,+2j1]s[*,*+2^j-1] 按字典序排序后 s[i,i+2j1]s[i,i+2^j-1] 的排名,sai,jsa'_{i,j} 同理。

倍增分为 logn\log n 轮,第 jj 轮得到 rki,j,sai,jrk'_{i,j},sa'_{i,j}

具体地,在第 jj 轮中,按 (rk,j1,rk+2j11,j1)\left(rk'_{*,j-1},rk'_{*+2^{j-1}-1,j-1}\right) 排序,得到 sai,jsa'_{i,j} 并推出 rki,jrk'_{i,j},注意需要去重。

最终 rkirki,logn,saisai,lognrk_i\gets rk'_{i,\log n},sa_i\gets sa'_{i,\log n},即倍增覆盖长度大于 nn,使 sufisuf_i 等价于 s[i,i+2j1]s[i,i+2^j-1] 后结束。

O(nlogn)O(n\log n) 做法

考虑到两个关键字的值域都为 O(n)O(n),可以使用基数排序,复杂度即可优化到 O(nlogn)O(n\log n)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <bits/stdc++.h>

using std::cin;
typedef long long ll;
constexpr int N=2e6+114;
int n;
std::string s;
int sa[N],rk[N],id[N],ork[N],buc[N];

inline void build()
{
int m=1<<7,k=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 j=1;;m=k,k=0,j<<=1)
{
for(int i=n-j+1;i<=n;i++)id[++k]=i;
for(int i=1;i<=n;i++)if(sa[i]>j)id[++k]=sa[i]-j;
std::fill(buc+1,buc+m*2+1,0);
std::copy(rk+1,rk+n*2+1,ork+1);
k=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];
for(int i=1;i<=n;i++)rk[sa[i]]=
(ork[sa[i-1]]==ork[sa[i]]&&ork[sa[i-1]+j]==ork[sa[i]+j])?k:++k;
if(k==n)break;
}
}

int main()
{
std::ios::sync_with_stdio(false);
cin.tie(nullptr);
cin>>s;
n=s.size();
s=' '+s;
build();
for(int i=1;i<=n;i++)printf("%d ",sa[i]);
return 0;
}

注意 rkrk' 严格满足排名的定义(即小于本身的不同元素的数量),可以维护 rki,jrk'_{i,j} 的值域 mm,当 m=nm=n 时直接退出。

同时对第二关键字的排序是不必要的,对于 i[n2j+1,n]i\in[n-2^j+1,n]rki+2j,j1=0rk'_{i+2^j,j-1}=0,直接将它们放置在 sai,jsa'_{i,j} 的开头,其他的按照 rki+2j,j1rk'_{i+2^j,j-1} 顺序即可。

htht 数组

lcp(i,j)\operatorname{lcp}(i,j)sufi,sufjsuf_i,suf_j 的最长公共前缀,htilcp(sai1,sai),ht10ht_i\gets|\operatorname{lcp}(sa_{i-1},sa_i)|,ht_1\gets 0

结论 1rkirkjrkkrk_i\le rk_j\le rk_k,则 lcp(i,j),lcp(j,k)lcp(i,k)|\operatorname{lcp}(i,j)|,|\operatorname{lcp}(j,k)|\ge\operatorname{lcp}(i,k)

结合图片容易理解,lcp\operatorname{lcp} 实际上是一个取区间 min\min,也就是说:

结论 2rki<rkjrk_i<rk_j,则 lcp(i,j)=mink=rk(i)+1rk(j)htk|\operatorname{lcp}(i,j)|=\min\limits_{k=rk(i)+1}^{rk(j)}ht_k

大区间的最小值自然不大于其子区间的最小值。

htht 数组所需的一个重要引理

ht(rki)ht(rki1)1ht(rk_i)\ge ht(rk_{i-1})-1

证明

i1i-1 的前驱 psa(rki11)p\gets sa(rk_{i-1}-1),根据定义 ht(rki1)=lcp(sa(rki11),sa(rki1))=lcp(p,i1)ht(rk_{i-1})=|\operatorname{lcp}(sa(rk_{i-1}-1),sa(rk_{i-1}))|=|\operatorname{lcp}(p,i-1)|

假设 ht(rki1)>0ht(rk_{i-1})>0,则显然

lcp(p+1,i)=lcp(p+1,i1+1)=lcp(sa(rki11)+1,sa(rki1)+1)=lcp(sa(rki11),sa(rki1))1=ht(rki1)1\begin{aligned} |\operatorname{lcp}(p+1,i)|&=|\operatorname{lcp}(p+1,i-1+1)|\\ &=|\operatorname{lcp}(sa(rk_{i-1}-1)+1,sa(rk_{i-1})+1)|\\ &=|\operatorname{lcp}(sa(rk_{i-1}-1),sa(rk_{i-1}))|-1\\ &=ht(rk_{i-1})-1 \end{aligned}

即同时删去第一个字符,lcp\operatorname{lcp}11

同时因为 ht(rki1)>0,rkp<rki1ht(rk_{i-1})>0,rk_p<rk_{i-1},不难发现 rkp+1<rkirk_{p+1}<rk_i。根据结论 1,

lcp(sa(rki1),i)lcp(p+1,i)ht(rki)ht(rki1)1\begin{aligned} |\operatorname{lcp}(sa(rk_i-1),i)|&\ge|\operatorname{lcp}(p+1,i)|\\ ht(rk_i)&\ge ht(rk_{i-1})-1\\ \end{aligned}

\square

以上内容可以结合下图理解。

1
2
3
4
5
6
7
8
9
10
11
s=aabbaba
p=sa(rk(i)-1)

ind rk suf p.s.
7 1 a p+1
1 2 aabbaba sa(rk_i-1)
5 3 aba i
2 4 abbaba
6 5 ba p
4 6 baba i-1
3 7 bbaba

这样,我们就有了均摊 O(n)O(n)htht 数组的优美做法。

1
2
3
4
5
6
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;
}