文章目录
- 前言
- 解析
- 后缀排序
- 优化1:基数排序
- 优化2:简化第一次排序
- 优化3:提前break
- 完整代码
- LCP与height
所谓后缀数组,就是存储后缀的数组
(逃)
前言
为什么一个算法,如此难以理解却依然是成为一个成熟OIer不可回避的必修课?
足以可见后缀家族功能的强大
首先,由于其本身的性质,后缀数组对字典序相关的问题十分擅长
同时,由于 heightheightheight 数组的众多优秀性质,它在处理公共串问题和 LCP 问题上也十分强大
(我目前SA的题加起来也没做上十道,所以这样的“总结”请选择性阅读)
解析
后缀排序
P3809 【模板】后缀排序
给出一个字符串,把所有后缀按照字典序排序
n≤106n\le10^6n≤106
考虑倍增
一开始子串长度为 111,每个位置的排名 rkirk_irki 就是自己位置的字符
然后在已知长度为 www 的所有子串的排名的情况下,以 rki+wrk_{i+w}rki+w 为第二关键字,rkirk_irki 为第一关键字排序,可以得到长度为 2w2w2w 的所有子串的排名(空串的排名视为负无穷)
每次用 sort 的话,时间复杂度 O(nlog2n)O(nlog^2n^)O(nlog2n)
优化1:基数排序
注意到这里的排序是关于大小的排序,且值域(排名)只有 O(n)O(n)O(n)
所以我们可以使用基数排序代替 sort,时间复杂度变成 O(nlogn)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:简化第一次排序
第一次是关于rkiwrk_{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.2s→0.8s2.2s\to 0.8s2.2s→0.8s)
完整代码
saisa_isai:排名为 iii 的后缀的编号
rkirk_irki:后缀 iii 的排名
#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);
#endifscanf(" %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
定义:
height(i)height(i)height(i) 表示后缀 saisa_isai 和后缀 sai−1sa_{i-1}sai−1 的最长公共前缀(lcp(sai,sai−1)lcp(sa_i,sa_{i-1})lcp(sai,sai−1))。特别的,lcp(1)=0lcp(1)=0lcp(1)=0
感性理解来说,把所有后缀按照字典序排序后,height(i)height(i)height(i) 就是相邻两个后缀的相同部分的长度。
引理1:lcp(i,j)=min(lcpi,k,lcpk,j)lcp(i,j)=\min (lcp_{i,k},lcp_{k,j})lcp(i,j)=min(lcpi,k,lcpk,j),对于任意的 i≤k≤ji\le k\le ji≤k≤j 均成立.
证明:
首先,min(lcpi,k,lcpk,j)\min (lcp_{i,k},lcp_{k,j})min(lcpi,k,lcpk,j) 是 kkk 与 i,ji,ji,j 共同的公共前缀,所以也必然是 i,ji,ji,j 的公共前缀,lcp(i,j)≥min(lcpi,k,lcpk,j)lcp(i,j)\ge\min (lcp_{i,k},lcp_{k,j})lcp(i,j)≥min(lcpi,k,lcpk,j)。
同时,由于字典序单调的性质,iii 变到 kkk 变化的前缀在 kkk 变化到 jjj 时必然不可能再变回来,否则 jjj 的字典序就比 kkk 小了,所以有 lcp(i,j)≤min(lcpi,k,lcpk,j)lcp(i,j)\le\min (lcp_{i,k},lcp_{k,j})lcp(i,j)≤min(lcpi,k,lcpk,j)。
综上,lcp(i,j)=min(lcpi,k,lcpk,j)lcp(i,j)=\min (lcp_{i,k},lcp_{k,j})lcp(i,j)=min(lcpi,k,lcpk,j),证毕。
引理2:heightrki≥heightrki−1−1height_{rk_i}\ge height_{rk_{i-1}}-1heightrki≥heightrki−1−1
证明:
rki−1≤1rk_{i-1}\le1rki−1≤1 时,显然成立
rki−1>1rk_{i-1}>1rki−1>1 时,设 rki−1−1=krk_{i-1}-1=krki−1−1=k(kkk 就是 i−1i-1i−1 按字典序排序后的前一个),那么:
若 i−1i-1i−1 和 kkk 的首字母不同, hi−1=0h_{i-1}=0hi−1=0 ,显然成立
若 i−1i-1i−1 和 kkk 的首字母相同,那么考虑字符串 k+1k+1k+1,由于k 去掉首字符变成 k+1,i-1 去掉首字母变成 i,所以 k+1k+1k+1 也一定在 iii 的前面,同时 lcp(k+1,i)=lcp(k,i−1)−1=heightrki−1−1lcp(k+1,i)=lcp(k,i-1)-1=height_{rk{i-1}}-1lcp(k+1,i)=lcp(k,i−1)−1=heightrki−1−1,由引理1,有 lcp(k+1,i)=min(lcp(k+1,rki−1),lcp(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)),故 lcp(i−1,i)≥heightrki−1−1lcp(i-1,i)\ge height_{rk{i-1}}-1lcp(i−1,i)≥heightrki−1−1,即 heightrki≥heightrki−1−1height_{rk_i}\ge height_{rk_{i-1}}-1heightrki≥heightrki−1−1
得证。
得出这个性质后,线性求 heightheightheight 的代码就不难写出了:
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!