后缀数组
SA
基数排序
一般采用 LSD(Least Significant Digital),从键值的最低位开始排序。
定义
记 \(suf(i)\) 为起始下标为 \(i\) 的后缀。
记 \(sa[i]\) 为排名第 \(i\) 的后缀的起始位置。
记 \(rk[i]\) 为 \(suf(i)\) 的排名。
对于一个长为 \(n\) 的字符串,求出其后缀数组 \(\{sa_n\}\).
倍增法
-
把每个字符当作长为 \(1\) 的后缀,按字符值进行编号。
-
将当前长度为 \(len\) 的后缀扩展至 \(2len\),按前半、后半部分的编号进行排序。
-
时间复杂度 \(O(n\log n)\),空间线性。
具体地,第二步中的前、后两部分会形成若干个二元组。
直接排序的复杂度为 \(O(n\log^2 n)\).
使用 LSD 进行基数排序,以第二维为第一关键字。
我们可以拿一个 \(tp\) 记录第二关键字中排名为 \(i\) 的数的位置。
#include<bits/stdc++.h>
#define N 1000010
using namespace std;
int n,m;char s[N];
int a[N],sa[N],rk[N],tp[N];
int get(char c){
if(c>='0'&&c<='9')return c-'0'+1;
if(c>='A'&&c<='Z')return c-'A'+11;
return c-'a'+37;
}
void init(){
n=strlen(s+1),m=62;
for(int i=1;i<=n;i++)
rk[i]=get(s[i]),tp[i]=i;
}
void LSD(){//基数排序
for(int i=1;i<=m;i++)a[i]=0;
for(int i=1;i<=n;i++)a[rk[i]]++;
for(int i=1;i<=m;i++)a[i]+=a[i-1];
for(int i=n;i;i--)sa[a[rk[tp[i]]]--]=tp[i];
}
void SA(){
for(int w=1,p=0;w<=n;m=p,p=0,w<<=1){
for(int i=n-w+1;i<=n;i++)tp[++p]=i;
//p记录最高排名,若p=n则无需排序
//n-w+1后面均无第二关键字,补0设成极小值排在前面
for(int i=1;i<=n;i++)
if(sa[i]>w)tp[++p]=sa[i]-w;
LSD(),swap(rk,tp),p=rk[sa[1]]=1;
//有rk[sa[i]]=i
for(int i=2;i<=n;i++)
rk[sa[i]]=(tp[sa[i]]==tp[sa[i-1]]&&
tp[sa[i]+w]==tp[sa[i-1]+w])?p:++p;
//两后缀相等排名不变
if(p==n)return;
}
}
int main(){
scanf("%s",s+1);
init(),LSD(),SA();
for(int i=1;i<=n;i++)
printf("%d ",sa[i]);
printf("\n");
return 0;
}
感性理解一下吧。时间复杂度 \(O(n\log n)\)
.
- 不要使用任何
vector
.
一些应用
-
最小循环移动位置
这个概念比较奇怪但是可以看看 P4051 [JSOI2007] 字符加密。
令 \(S\leftarrow SS\),求一遍 SA 即可。
本题字符集包含了字符所以可以让程序里的 \(m\) 取一个大值。
main 函数:
int main(){
scanf("%s",_s+1),_n=strlen(_s+1);
for(int i=1;i<=_n;i++)
s[i]=s[i+_n]=_s[i];
init(),LSD(),SA();
for(int i=1;i<=n;i++){
if(sa[i]<=_n)
printf("%c",s[sa[i]+_n-1]);
}
printf("\n");
return 0;
}
-
匹配子串
在线地在主串 \(T\) 里寻找模式串 \(S\).
做出 \(T\) 的 SA,若 \(S\) 在 \(T\) 中出现,那么 \(S\) 是 \(T\) 的某个后缀的前缀。
在排序过的后缀里二分,一次比较的时间为 \(O(|S|)\),故总时间 \(O(|S|\log|T|)\).
若求出现次数,则其在 SA 数组中相邻,同样上一次二分即可。
-
取首尾最小化字典序
P2870 [USACO07DEC] Best Cow Line G
暴力做法即单次 \(O(n)\) 比较当前串与反串的大小。考虑优化。
一个十分方便的想法是将原串与反串拼接,以 #
隔开,这样就能够做到 \(O(n\log n)\) 预处理,单次 \(O(1)\) 的复杂度。
main 函数:
int main(){
scanf("%d",&_n),getchar();
for(int i=1;i<=_n;i++){
scanf("%c",&s[i]),getchar();
s[_n*2+1-i+1]=s[i];
}
s[_n+1]='#';
init(),LSD(),SA();
for(int i=1,l=1,r=_n;i<=_n;i++){
printf("%c",rk[l]<rk[n+1-r]?s[l++]:s[r--]);
if(i%80==0)printf("\n");
}
return 0;
}
height 数组
-
LCP(最长公共前缀)
这里我们记 \(lcp(i,j)\) 为后缀 \(i\) 和 \(j\) 的 LCP(的长度)。
-
height 数组的定义
\(height[i]=lcp(sa[i],sa[i-1])\),即第 \(i\) 名的后缀与 \(i-1\) 名的后缀的 LCP。
\(height[1]\) 视作 \(0\).
-
height 数组求解
引理:\(height[rk[i]]\ge height[rk[i-1]]-1\)
证明:当 \(height[rk[i-1]]\le 1\) 时显然成立。下面对 \(height[rk[i-1]]>1\) 的情况进行讨论。
由 height 数组的定义有 \(lcp(sa[rk[i-1]],sa[rk[i-1]-1])=height[rk[i-1]]>1\).
也就是后缀 \(i-1\) 和后缀 \(sa[rk[i-1]-1]\) 有长为 \(height[rk[i-1]]\) 的 LCP。用一个字符 \(a\) 和一个串 \(A\) 表示它。
即 \(LCP=aA\),且 \(|A|=height[rk[i-1]]-1\).
后缀 \(i-1\) 可表示为 \(aAD\),后缀 \(sa[rk[i-1]-1]\) 可表示为 \(aAB\),且有 \(B<D\),\(D\) 非空,\(B\) 可空。
后缀 \(i\) 可表示为 \(AD\),存在后缀 \((sa[rk[i-1]-1]+1)\) 为 \(AB\).
由于后缀 \(sa[rk[i]-1]\) 在大小排名上仅比后缀 \(sa[rk[i]]=i\) 小一位,\(AB<AD\),所以有 \(AB\le\text{后缀}sa[rk[i]-1]<AD\).
显然后缀 \(i\) 和后缀 \(sa[rk[i]-1]\) 有公共前缀 \(A\).
即 \(height[rk[i]]\ge height[rk[i-1]]-1\).
实在看不懂就感性理解了。
线性复杂度代码实现
根据引力暴力求解。
for(int i=1,k=0;i<=n;i++){
if(!rk[i])continue;
if(k)k--;
while(s[i+k]==s[sa[rk[i]-1]+k])k++;
height[rk[i]]=k;
}
\(k\) 恒小于等于 \(n\),最多减 \(n\) 次,加 \(2n\) 次。
故复杂度线性。
一些应用
-
两子串 LCP
若 \(height\) 一直大于某个数,则前这么多位一直不变。反之,由于后缀排过序,变化的位不可能变回来。
由此两子串的 LCP 转化成了一个 RMQ 问题。
-
比较两子串大小
记两者为 \(A=s[a\sim b]\),\(B=s[c\sim d]\).
若 \(lcp(a,c)\ge min(|A|,|B|)\),\(A<B\Longleftrightarrow |A|<|B|\).
否则,\(A<B\Longleftrightarrow rk[a]<rk[c]\).
-
不同字串数目
子串即后缀的前缀,枚举每个后缀,计算前缀总数再减掉重复的。
前缀总数即 \(\displaystyle\frac{n(n+1)}{2}\).
将后缀排序后枚举,新增的子串是除了与上一个后缀的 LCP 剩下的前缀。
答案是
-
出现次数至少为 \(k\) 的最长子串
P2852 [USACO06DEC] Milk Patterns G
取所有相邻的 \(k-1\) 个 \(height\) 的 \(\min\) 的 \(\max\).
配合单调队列实现。
--