后缀数组(SA)and后缀自动机(SAM) 学习笔记&
一.后缀排序
后缀数组(𝑆𝑢𝑓𝑓𝑖𝑥 𝐴𝑟𝑟𝑎𝑦,𝑆𝐴)是一个处理字符串问题非常有用的数据结构。其基于将字符串的所有后缀按照字典序排序(称为后缀排序),维护后缀之间的最长公共前缀,将许多字符串问题转化为区间的问题求解
后缀数组定义: 表示字典序从小到大排名为𝑖的后缀的起始位置。
定义空字符的字典序小于任何其他字符。
#include<iostream>
#include<cstdio>
#include<cstring>
#define maxN 1000010
using namespace std;
char s[maxN];
int a[maxN],rk[maxN],sa[maxN],tp[maxN],tax[maxN],n,m=122;
//a是暂存数组,rk第i位的排名,sa是排名为i的位置,tp是第二关键字辅助用的,tax是桶数组;
inline void read(){
scanf("%s",&s);
n=strlen(s);
for(int i=0;i<n;i++) a[i+1]=s[i];
}
inline void Rsort(){
for(int i=0;i<=m;i++)tax[i]=0;
for(int i=1;i<=n;i++)tax[rk[tp[i]]]++;
for(int i=1;i<=m;i++)tax[i]+=tax[i-1];
for(int i=n;i>=1;i--)sa[tax[rk[tp[i]]]--]=tp[i];
}
inline void Suffix(){
for(int i=1;i<=n;i++)rk[i]=a[i],tp[i]=i;
Rsort();
for(int k=1;k<=n;k<<=1){
int num=0;
for(int i=n-k+1;i<=n;i++) tp[++num]=i;
for(int i=1;i<=n;i++) if(sa[i]>k) tp[++num]=sa[i]-k;
Rsort(),swap(rk,tp),num=rk[sa[1]]=1;
for(int i=2;i<=n;i++)
rk[sa[i]]=(tp[sa[i]]==tp[sa[i-1]]&&tp[sa[i]+k]==tp[sa[i-1]+k])?num:++num;
if(num==n)break;
m=num;
}
}
int main(){
read(),Suffix();
for(int i=1;i<=n;i++)printf("%d ",sa[i]);
}
update 2020.1.7
最长公共前缀
后缀数组在解决问题的时候几乎都要求一个最长公共前缀(𝐿𝑜𝑛𝑔𝑒𝑠𝑡𝐶𝑜𝑚𝑚𝑜𝑛𝑃𝑟𝑒𝑓𝑖𝑥,𝐿𝐶𝑃)
后缀数组将一个字符串所有的后缀处理好,那么后缀之间的前缀关系就非常重要。比如说经典的字符串匹配问题(𝐾𝑀𝑃可以𝑂(𝑛)解决),如果用后缀数组来做,我们就要把文本串和模式串接在一起(模式串接在后面),然后整个做后缀排序。匹配即找与模式串相等的子串,等价于在大串的所有后缀中寻找一个后缀,使得它与模式串(也是一个后缀)的𝐿𝐶𝑃长度等于模式串长度。而后缀排序后相同的前缀肯定排在一起。因此很多时候通过𝐿𝐶𝑃,字符串的问题能够转化为区间问题,因此线段树、莫队等数据结构就有了用武之地。
定义
定义𝐿𝐶𝑃(𝑖,𝑗):
表示后缀𝑖与后缀𝑗的𝐿𝐶𝑃长度。
定义ℎ𝑒𝑖𝑔ℎ𝑡:
数组表示两个排名相邻的后缀的𝐿𝐶𝑃。即ℎ𝑒𝑖𝑔ℎ𝑡𝑖=𝐿𝐶𝑃(𝑠𝑎𝑖−1,𝑠𝑎𝑖)
求Height数组
两个后缀的𝐿𝐶𝑃一定其排名那段ℎ𝑒𝑖𝑔ℎ𝑡中的最小值。这就是刚才所说的区间问题。因此只要高效地求出ℎ𝑒𝑖𝑔ℎ𝑡数组就能利用𝑆𝑇表来求得𝐿𝐶𝑃了。
我们不按排名顺序来求,而是按照后缀的位置从前到后来求ℎ𝑒𝑖𝑔ℎ𝑡
假设我们已经求得后缀𝑖−1与排在它前一个的后缀𝑘的ℎ𝑒𝑖𝑔ℎ𝑡,即已经求得ℎ𝑒𝑖𝑔ℎ𝑡[𝑟𝑛𝑘[i]−1]
接下来要考虑后缀𝑖和排在它前一个的后缀𝑝
后缀𝑖相对于后缀𝑖−1只少了第一个字符。而既然肯定的是后缀𝑘+1与后缀𝑖的𝐿𝐶𝑃为ℎ𝑒𝑖𝑔ℎ𝑡𝑟𝑛𝑘𝑖−1−1,那么𝐿𝐶𝑃(𝑝,𝑖)的长度至少会等于ℎ𝑒𝑖𝑔ℎ𝑡𝑟𝑛𝑘𝑖−1−1。因此有ℎ𝑒𝑖𝑔ℎ𝑡𝑟𝑛𝑘𝑖≥ℎ𝑒𝑖𝑔ℎ𝑡𝑟𝑛𝑘𝑖−1−1
而接下来多出的部分暴力匹配过去即可。因为我们的ℎ𝑒𝑖𝑔ℎ𝑡
近乎于单调递增(每次只减1),也就保证了求ℎ𝑒𝑖𝑔ℎ𝑡数组的复杂度是近似为𝑂(𝑛)的。