[笔记]后缀数组
模板测试链接
大致思想
利用倍增的思想解决后缀排序问题.
设上一轮比较长度为 \(k\),那么这一轮比较长度为 \(2k\),我们只取每个后缀的 \(2k\) 个,并将他们每个后缀划分为前 \(k\) 个字符的部分和后 \(k\) 个字符的部分,如果某个长度小于等于 \(k\)(也就是没有第二段),我们规定将其第二段看作无限小.
还有一些规定和定义:
-
后缀 \(i\):指后缀 \(s[i...n]\);
-
将某个后缀的前 \(k\) 个字符的部分称作一关键字,后 \(k\) 个字符的部分称作二关键字;
-
\(n\) :变量,字符串长度;
-
\(x_i\) :数组,后缀 \(i\) 的一关键字属于哪个等级的(如果有相同视为同一等级);
-
\(y_i\) :数组,下标为排名,含义是二关键字排名为 \(i\) 的是哪个后缀;
-
\(sa_i\) :数组,在比完 \(k\) 个字符后,排名为 \(i\) 的后缀的编号;
接下来对于思想进行一些说明.
排序采用基数排序,先将二关键字排序,再排一关键字.
假设我们已经得到上一轮的 \(sa_i\),并在上一轮中将这一轮的 \(x\) 数组处理出来了.
首先,我们可以将 \(sa_i\) 对应下标往前平移 \(k\) 个得到 \(sa_i-k\),即在我们这一轮中,上一轮的 \(sa_i\) 对应的字符串就是这一轮第 \(sa_i-k\) 个后缀的二关键字,将其有序地放进 \(y\) 数组中去.
对于没有二关键字的第 \([n-k+1,n]\) 个后缀,我们直接将其放进 \(y\) 的最前面,顺序任意,因为没有二关键字就将其视为最小.
这个时候,我们得到这个阶段有序的 \(y_i\),接下来我们要得到这个阶段的 \(sa_i\).
我们将 \(y_i\) 对应的后缀的一关键字按照等级从大到小,从后往前(其实就是从小到大,从前往后)放入 \(sa_i\),这个时候就有序了.
考虑我们要为下一阶段的 \(x_i\) 做准备,下个阶段的一关键字其实就是这个阶段的 一关键字+二关键字.
那么,我们比较两个下阶段一关键字是否在同一等级其实就是比较这个阶段的一关键字和二关键字是否在同一等级.
代码按照这个实现即可.
代码
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
namespace Elaina{
#define rep(i,l,r) for(int i=l,i##_end_=r;i<=i##_end_;++i)
#define fep(i,l,r) for(int i=l,i##_end_=r;i>=i##_end_;--i)
#define fi first
#define se second
#define Endl putchar('\n')
#define writc(x,c) fwrit(x),putchar(c)
typedef long long ll;
typedef pair<int,int> pii;
typedef unsigned long long ull;
typedef unsigned int uint;
template<class T>inline T Max(const T x,const T y){return x<y?y:x;}
template<class T>inline T Min(const T x,const T y){return x<y?x:y;}
template<class T>inline T fab(const T x){return x<0?-x:x;}
template<class T>inline void getMax(T& x,const T y){x=Max(x,y);}
template<class T>inline void getMin(T& x,const T y){x=Min(x,y);}
template<class T>T gcd(const T x,const T y){return y?gcd(y,x%y):x;}
template<class T>inline T readin(T x){
x=0;int f=0;char c;
while((c=getchar())<'0' || '9'<c)if(c=='-')f=1;
for(x=(c^48);'0'<=(c=getchar()) && c<='9';x=(x<<1)+(x<<3)+(c^48));
return f?-x:x;
}
template<class T>void fwrit(const T x){
if(x<0)return putchar('-'),fwrit(-x);
if(x>9)fwrit(x/10);putchar(x%10^48);
}
}
using namespace Elaina;
const int maxn=1e6;
char s[maxn+5];
int n,m;
inline void init(){
scanf("%s",s+1);
n=strlen(s+1);
}
/** @brief 后缀 i 的一关键字属于哪个等级的, *2是为了防止后面使用的时候越界*/
int x[maxn*2+5];
/** @brief 下标为排名, 值为二关键字排名为 i 的是哪个后缀(部分代码中含义会变), *2是为了防止后面使用的时候越界*/
int y[maxn*2+5];
/** @brief 排名统计, 桶的每个部分的划分, 由于最多可能会有 n 个等级, 所以应该开 maxn 大小*/
int c[maxn+5];
/** @brief 排名为 i 的后缀的编号*/
int sa[maxn+5];
inline void getsa(){
// 初始化
m=122;
rep(i,1,n)++c[x[i]=s[i]];
rep(i,2,m)c[i]+=c[i-1];
fep(i,n,1)sa[c[x[i]]--]=i;
for(int k=1;k<=n;k<<=1){
int sz=0;// 填到 sa 的第几位了
// 有些没有二关键字的, 直接放入 y
rep(i,n-k+1,n)y[++sz]=i;
// 将 sa 转换成另一个串的二关键字
rep(i,1,n)if(sa[i]>k)y[++sz]=sa[i]-k;
// 清空统计数量
rep(i,0,m)c[i]=0;
// 统计每个等级有多少数字
rep(i,1,n)++c[x[i]];
// 将统计数字转换为对应桶中分配的区间
rep(i,2,m)c[i]+=c[i-1];
// 从大到小枚举二关键字
// 为什么从大到小实际上是因为桶的编号是倒着放的
// 将二关键字对应后缀的一关键字按照一关键字的等级放入 sa
fep(i,n,1)sa[c[x[y[i]]]--]=y[i],y[i]=0;
// 结合上一排 y[i]=0
// 相当于将 x 的值复制到 y 中
// 然后将 x 清空
// 此时 y 的含义为 后缀 i 的一关键字对应的哪个等级
swap(x,y);
// 接下来要处理下一次使用的 x 对应的等级
// 此时的 一关键字 + 二关键字 为下一次的一关键字
// 肯定排名最前面的, 在下一次的等级中肯定是第一等
x[sa[1]]=1;
int level=1;
// 这里就是所谓的 "避免越界" 的地方
// 如果他们这个阶段的一关键字等级相等, 并且二关键字等级也相等, 那么它们在下一次就是同一等级
// 否则, 由于我们之前已经按一关键字拍好, 所以后面的等级一定会大一些
rep(i,2,n)x[sa[i]]=(y[sa[i]]==y[sa[i-1]] && y[sa[i]+k]==y[sa[i-1]+k])?level:++level;
if(level==n)break;// 小小的剪枝
m=level;
}
}
signed main(){
init();
getsa();
rep(i,1,n)writc(sa[i],' ');
return 0;
}
扩展应用——最长公共前缀(LCP)
在某个给定的字符串 \(s\) 中,如果我们要求某两个后缀的 \(LCP\),我们应该怎么做?
这里有几个思路:
暴力永垂不朽
对于单次询问,我们可以直接暴力匹配,这样做的时间复杂度为 \(\mathcal O(n)\),对于多组询问,这样做的复杂度是 \(\mathcal O(Tn)\),对于 \(T,n\) 比较大的时候,这个做法可以上天了 但是很多时候我们都是这么做的,不是吗?
基于 sa[] 数组进行优化
如果我们得到这两个串 \(i,j\) 在 \(sa[]\) 中的下标,我们可以比较快地求出 \(LCP(i,j)\),具体需要几个辅助的东西:
为了方便说明,我们有以下定义:
- \(rk_i\):后缀 \(i\) 在 \(sa\) 中的排名;
- \(h_i\):下标为后缀编号,后缀 \(i\) 和它排名之前的后缀的 \(LCP\),即 \(LCP(i,sa_{rk_i-1})\);
- \(\text{height}_i\):下标为排名,排名为 \(i\) 的后缀与它排名之前的后缀的 \(LCP\),即 \(LCP(sa_i,sa_{i-1})\);
基于定义,我们有几个基本关系:
- \(h_i=\text{height}_{rk_i}\);
- \(\text{height}_i=h_{sa_i}\);
- \(i=rk_{sa_i}=sa_{rk_i}\);
- \(\text{height}_1=0\);
对于两个排名为 \(x,y(x<y)\) 的后缀,显然有
我们称其为 \(\text{LCP Theorem}\).
此处 \(LCP()\) 使用的并非下标,而是排名,如果用 \(\text{height}\) 表示,就是
这就是 \(rmq\) 了,我们如果得到了 \(\text{height}\) 就可以单个询问 \(\mathcal O(\log)\) 地做.
但是如何求 \(\text{height}\) ?考虑 \(\text{height}\) 和 \(h\) 有关联,我们考虑先求得 \(h\),再转换为 \(\text{height}\).
关于快速求 \(h\),我们有个定理,用不等式呈现是这样的
现在我们考虑怎么证明这个定理.
我们先定义变量 \(k=sa_{rk_{i-1}-1}\),即后缀 \(k\) 是后缀 \(i-1\) 排名之前的串,显然不保证 \(k=i-2\).
接下来,我们考虑后缀 \(k+1\) 与后缀 \(i\) 的 \(LCP\) 长度的性质,为什么是这两个?由于下标差,后缀 \(k+1\) 就是后缀 \(k\) 去掉首字母,而后缀 \(i\) 是 \(i-1\) 去掉首字母.
分情况讨论:
- 如果 \(h_{i-1}=0\),即后缀 \(k\) 与 \(i\) 的首字符不同,那么我们不能确定 \(rk_{k+1}\) 与 \(rk_i\) 的相对关系,但是由于 \(h_{i-1}=0\),所以一定有
- 如果 \(h_{i-1}\ge 1\),即后缀 \(k\) 与 \(i\) 的首字符相同,由于后缀 \(k+1\) 是后缀 \(k\) 去掉首字母得到,后缀 \(i\) 是 \(i-1\) 去掉首字母得到,所以一定有 \(rk_{k+1}<rk_i\),并且有 \(LCP(k+1,i)=h_{i-1}-1\),由于 \(LCP(k+1,i)=h_{i-1}-1\),我们由 \(\text{LCP Theorem}\) 可以得到
所以这个定理成立.
那么我们利用 \(h\) 便可求得 \(\text{height}\),时间复杂度 \(\mathcal O(n+T\log n)\).
代码实现
int height[maxn+5],rk[maxn+5];
inline void getheight(){
int h=0;
rep(i,1,n)rk[sa[i]]=i;
// 以后缀位置从小到大枚举
rep(i,1,n){
if(rk[i]==1)continue;
if(h)--h;//h[i]>=h[i-1]-1
/** @brief 得到 i 排名之前的后缀*/
int j=sa[rk[i]-1];
while(i+h<=n && j+h<=n && s[i+h]==s[j+h])++h;
height[rk[i]]=h;
}
}