后缀数组——倍增算法
1 简介
后缀数组是后缀树的一个有力的替代品,倍增算法的复杂度是 \(O(n\log n)\) ,这个复杂度在大多数情况下不会成为瓶颈,sa-is 算法留着以后再学。
后缀数组所做的事情是把所有的后缀拿出来,然后按照字典序排序。
2 算法讲解
我们先把所有单个字符排序,实际上是给所有后缀的第一个字符排序。
然后我们在给所有后缀的前 \(2\) 个字符排序。
然后我们在给所有后缀的前 \(4\) 个字符排序。
直到所有排名两两不同。
注意每一次当我们给所有后缀的 \(2^i\) 排序的时候,我们实际可以利用我们上一次给 \(2^{i-1}\) 排序的结果。如下图:
这里需要实现一个双关键字排序,我们首选用基数排序加计数排序。
再用基数排序实现双关键字排序的时候注意要先排序第二关键字,然后再排序第一关键字。
3 实现分析
我们首先对一开始的数组进行预处理:
for(int i=1;i<=n;i++) cnt[Rank[i]=s[i]]++;
for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--) sa[cnt[Rank[i]]--]=i;
一个简单的计数排序,我们就可以完成对单个字符的排序。这里不用关心值域,因为我们后面会再做调整。
排序代码:
inline bool Cmp(int x,int y,int w){
return LastRank[x]==LastRank[y]&&LastRank[x+w]==LastRank[y+w];
}
inline void Sort(){
for(int i=1;i<=n;i++) cnt[Rank[i]=s[i]]++;
for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--) sa[cnt[Rank[i]]--]=i;
for(int w=1;;w<<=1,m=p){
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));
for(int i=1;i<=n;i++) cnt[px[i]=Rank[id[i]]]++;
for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--) sa[cnt[px[i]]--]=id[i];
memcpy(LastRank,Rank,sizeof(Rank));p=0;
for(int i=1;i<=n;i++) Rank[sa[i]]=Cmp(sa[i],sa[i-1],w)?p:++p;
if(p==n) break;
}
}
我们考虑目前倍增是 \(w\),那么在 \(n-w\) 之后的一定都没有第二关键字,也就是说,第二关键字为空,所以如果我们首先按照第二关键字排序的话,一定是把这些放在前面。然后我们考虑枚举这个第二关键字,按照排名,把相对应的第一关键字放入 \(id\) 数组中,这样做的目的是使访问内存连续,优化常数。
然后我们对第一关键字进行排序,\(px\) 数组的目的也是为了优化常数,运用基数排序就可以,然后我们考虑更新 \(Rank\),这里需要注意,如果当前的和上一个的之前的排名第一关键字和第二关键字相等,这个时候就需要让这个后缀的排名相等。\(p\) 代表的是值域,可以用来缩小 \(m\) 的范围。如果 \(p\) 和 \(n\) 相等,就没有两个相等的字符串了,这个时候再排序下去也没有意义,直接退出即可。
我们加上 \(Cmp\) 代码的目的也是为了使内存连续。
4 总代码
#include<bits/stdc++.h>
#define dd double
#define ld long double
#define ll long long
#define uint unsigned int
#define ull unsigned long long
#define N 1000100
#define M number
using namespace std;
const int INF=0x3f3f3f3f;
template<typename T> inline void read(T &x) {
x=0; int f=1;
char c=getchar();
for(;!isdigit(c);c=getchar()) if(c == '-') f=-f;
for(;isdigit(c);c=getchar()) x=x*10+c-'0';
x*=f;
}
int n,m,p,Rank[N],sa[N],id[N],px[N],cnt[N],LastRank[N<<1];
char s[N];
inline bool Cmp(int x,int y,int w){
return LastRank[x]==LastRank[y]&&LastRank[x+w]==LastRank[y+w];
}
inline void Init(){
scanf("%s",s+1);n=strlen(s+1);m=1000;
}
inline void Sort(){
for(int i=1;i<=n;i++) cnt[Rank[i]=s[i]]++;
for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--) sa[cnt[Rank[i]]--]=i;
for(int w=1;;w<<=1,m=p){
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));
for(int i=1;i<=n;i++) cnt[px[i]=Rank[id[i]]]++;
for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--) sa[cnt[px[i]]--]=id[i];
memcpy(LastRank,Rank,sizeof(Rank));p=0;
for(int i=1;i<=n;i++) Rank[sa[i]]=Cmp(sa[i],sa[i-1],w)?p:++p;
if(p==n) break;
}
}
inline void Print(){
for(int i=1;i<=n;i++) printf("%d ",sa[i]);
}
int main(){
Init();Sort();Print();return 0;
}
5 LCP 与 Height 数组
LCP 的定义是最长公共前缀,以下用 \(LCP(i,j)\) 表示编号为 \(i\) 的前缀和编号为 \(j\) 的前缀的 LCP,我们令 \(Height_i=LCP(sa_i,sa_{i-1})\),而 \(Height_1\) 可以视作 \(0\)。我们考虑如何求 \(Height\) 数组。
- 引理:\(Height_{rk_i}\ge Height_{rk_{i-1}}-1\)。
证明:如果 \(Height_{rk_{i-1}-1}\) 小于等于 \(1\),那么上面这个式子显然成立,这是因为 \(Height_i\ge 0\)。
如果 \(Height_{rk_{i-1}-1}>1\),我们考虑设后缀 \(i-1\) 为 \(aAD\),那么后缀 \(i\) 就是 \(AD\),其中 \(A\) 是一个长度为 \(Height_{rk_{i-1}}-1\) 的字符串,由于 \(Height\) 数组的定义,我们考虑后缀 \(sa_{rk_{i-1}-1}\) 这个字符串一定是 \(aAB\) 的形式,所以 \(sa_{rk_{i-1}-1}+1\) 这个后缀一定是 \(AB\) 的形式,所以这个后缀一定排在后缀 \(i\) 的前面,所以我们可以知道 \(Height_{rk_i-1}\) 一定包含字符串 \(A\),所以结论成立。
我们利用上面这个东西就可以 \(O(n)\) 的来求 \(Height\) 数组了。
代码:
inline void GetHeight(){
for(int i=1,k=0;i<=n;i++){
if(k) k--;
while(s[i+k]==s[sa[rk[i-1]-1]+k]) k++;
Height[rk[i]]=k;
}
}
有一个结论是 \(LCP(sa_i,sa_j)=\min_{i+1\le k\le j}Height_k\),这个可以感性理解一下,如果 \(Height\) 一直大于某个值,那么这一段就一直有,反之,肯定是在一个后缀中改变。
还有一个结论是本质不同子串个数是 \(n(n-1)/2-\sum_{i=2}^ nHeight_i\)
考虑证明,其实比较简单,我们考虑首先计数,然后去重,所谓去重,就是去掉两个后缀的相同前缀,但我们其实不需要 \(n^2\) 枚举后缀,我们只需要把它们都加起来,考虑 \(LCP(sa_i,sa_j)\),设值为 \(x\),区间 \(i,j\) 中每一个都有这样的一个 \(x\),我们考虑这个东西已经被上面那个式子所包含。