后缀树组 学习笔记
0xFF 一些备注
本篇博客所有证明基本略过,主要总结后缀树组的应用
引用有 [2009]后缀数组——处理字符串的有力工具 by. 罗穗骞、OI wiki、日报 的大量内容
实现方面本篇博客只会倍增方法
0x00 一些定义
-
第 \(i\) 个后缀指的是首字符在 \(i\) 位置的后缀
-
这里排序的关键字是字典序,定义空字符最小
-
\(sa[i]\) 表示排名为 \(i\) 的后缀是第几个后缀
-
\(rk[i]\) 表示第 \(i\) 个后缀的排名是多少
-
\(lcp(i,j)\) 表示后缀 \(i\) 和后缀 \(j\) 的最长公共前缀长度
-
\(height[i]\) 表示 \(lcp(sa[i],sa[i-1])\)
0x01 后缀排序
目标:将一个字符串的 \(n\) 个后缀按照字典序大小排序
实现:按照倍增的思想,分别排序前 \(2^0, 2^1, 2^2, 2^3……\) 个字符。上一次排序过后,以上一次排序为第二关键字,当前层为第一关键字继续排序,总复杂度 \(O(nlogn)\)
至于怎样 \(O(n)\) 排序,可以采用基数排序,因为上一层可以顺便帮助这一层离散化
具体来讲,开 \(n\) 个桶,将值放进去,然后自然而然就排好序了
直接放代码,照着代码来具体讲:
void get_sa(){
for(int i=1;i<=n;i++)c[x[i]=s[i]]++;
for(int i=1;i<=m;i++)c[i]+=c[i-1];
for(int i=n;i>=1;i--)sa[c[x[i]]--]=i;
for(int k=1;k<=n;k<<=1){
int num=0;
for(int i=n-k+1;i<=n;i++)y[++num]=i;
for(int i=1;i<=n;i++){
if(sa[i]>k)y[++num]=sa[i]-k;
}
for(int i=1;i<=m;i++)c[i]=0;
for(int i=1;i<=n;i++)c[x[i]]++;
for(int i=1;i<=m;i++)c[i]+=c[i-1];
for(int i=n;i>=1;i--)sa[c[x[y[i]]]--]=y[i],y[i]=0;
swap(x,y);
x[sa[1]]=1;
num=1;
for(int i=2;i<=n;i++){
x[sa[i]]=(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k])?num:++num;
}
if(num==n)break;
m=num;
}
return ;
}
int main(){
m=122;
scanf("%s",s+1);
n=strlen(s+1);
get_sa();
}
和 \(c[i]\) 数组有关的是基数排序的过程,不再赘述
for(int i=n-k+1;i<=n;i++)y[++num]=i;
for(int i=1;i<=n;i++){
if(sa[i]>k)y[++num]=sa[i]-k;
}
这两行的意思是这样的,由于从 \(n-k+i\) 开始,由于长度不够,所以子串为空,自然最短,而剩下的则有 \(sa[i]\) 决定,这样省去了第二关键字排序的过程
0x02 lcp问题的求解
直接放一些我也不会证的结论:
利用最后一个结论,可以快速 \(O(n)\) 求出 \(height\) 数组
void get_height(){
for(int i=1;i<=n;i++)rk[sa[i]]=i;
for(int i=1,k=0;i<=n;i++){
if(rk[i]==1)continue;
if(k)k--;
int j=sa[rk[i]-1];
while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k])k++;
height[rk[i]]=k;
}
return ;
}
以上就是后缀数组题板子的标配,目前还没有见过需要动板子的题,背会就行了
0x03 简单应用
以下开始疯狂抄袭论文例题:
可重叠最长重复子串
给定一个字符串,求最长重复子串,这两个子串可以重叠。
由于后缀排完序以后重复子串一定是在相邻后缀中的,求最长,是经典的 \(lcp\) 问题,找到最大的 \(height\) 值即可
不可重叠最长重复子串
给定一个字符串,求最长重复子串,这两个子串不能重叠。
首先二分答案,然后这里用到一个非常有用的套路:
由于求解 \(lcp\) 问题是一直取 \(min\) 的过程,一旦有一组极小值产生了断层,则无法挽回
既然二分答案后子串的长度已经确定,那么将 \(lcp\) 小于答案的地方隔断,将后缀们分成若干组,这样在一组内的后缀只要不重叠就满足条件
对于重叠的判断:每一组内找出实际串里最靠前和最靠后的位置,即 \(sa\) 的最值,若差比答案大则没有重叠
从这个题里可以引申出另一道的做法:
首先这里的相似即是 \(lcp\) 运算,可以按照类似的方法分组,在一组里面的都是满足相似关系的
考虑多组询问,如果按照题目的顺序,当相似值加一时,意味着有新的位置不满足条件需要拆开,而拆开这个操作是不好维护的
于是用倒序,则变成合并,转化成一个联通性问题,用并查集维护
对于第二问,并查集顺便统计连通块最值,由于有负权值,需要分类讨论负数的情况
代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int maxn=3e5+5;
const int inf=0x3f3f3f3f3f3f3f3f;
int n,m,c[maxn],sa[maxn],x[maxn],y[maxn],rk[maxn],height[maxn],fa[maxn],siz[maxn],sum,val=-inf,a[maxn],mx[maxn],cmx[maxn],mn[maxn],cmn[maxn];
vector<int>ex[maxn];
pair<int,int>ans[maxn];
char s[maxn];
int read(){
int x=0,f=1;
char ch=getchar();
while(!isdigit(ch)){
if(ch=='-')f=-1;
ch=getchar();
}
while(isdigit(ch)){
x=x*10+ch-48;
ch=getchar();
}
return x*f;
}
void get_sa(){
for(int i=1;i<=n;i++)c[x[i]=s[i]]++;
for(int i=2;i<=m;i++)c[i]+=c[i-1];
for(int i=n;i;i--)sa[c[x[i]]--]=i;
for(int k=1;k<=n;k<<=1){
int num=0;
for(int i=n-k+1;i<=n;i++)y[++num]=i;
for(int i=1;i<=n;i++){
if(sa[i]>k)y[++num]=sa[i]-k;
}
for(int i=1;i<=m;i++)c[i]=0;
for(int i=1;i<=n;i++)c[x[i]]++;
for(int i=2;i<=m;i++)c[i]+=c[i-1];
for(int i=n;i;i--)sa[c[x[y[i]]]--]=y[i],y[i]=0;
swap(x,y);
x[sa[1]]=1;
num=1;
for(int i=2;i<=n;i++){
x[sa[i]]=(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k])?num:++num;
}
if(num==n)break;
m=num;
}
return ;
}
void get_height(){
for(int i=1;i<=n;i++)rk[sa[i]]=i;
for(int i=1,k=0;i<=n;i++){
if(rk[i]==1)continue;
if(k)k--;
int j=sa[rk[i]-1];
while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k])k++;
height[rk[i]]=k;
}
return ;
}
int find(int x){
return fa[x]==x?x:fa[x]=find(fa[x]);
}
int calc_val(int x){
return x*(x-1)/2;
}
void calc(int p){
for(int i=0;i<ex[p].size();i++){
int x=ex[p][i];
int xx=find(x-1);
int yy=find(x);
sum+=calc_val(siz[xx]+siz[yy])-calc_val(siz[xx])-calc_val(siz[yy]);
fa[xx]=yy;
siz[yy]+=siz[xx];
int d[6]={-inf,mx[xx],cmx[xx],mx[yy],cmx[yy]};
sort(d+1,d+5);
// for(int i=1;i<=4;i++)cout<<d[i]<<" ";
// cout<<endl;
mx[yy]=d[4];
cmx[yy]=d[3];
int b[6]={-inf,mn[xx],cmn[xx],mn[yy],cmn[yy]};
sort(b+1,b+5);
// for(int i=1;i<=4;i++)cout<<b[i]<<" ";
// cout<<endl;
mn[yy]=b[1];
cmn[yy]=b[2];
val=max(val,max(mx[yy]*cmx[yy],mn[yy]*cmn[yy]));
}
ans[p]=make_pair(sum,val);
if(val==-inf)ans[p].second=0;
return ;
}
signed main(){
n=read();
scanf("%s",s+1);
for(int i=1;i<=n;i++)a[i]=read();
m=122;
get_sa();
get_height();
for(int i=2;i<=n;i++)ex[height[i]].push_back(i);
for(int i=1;i<=n;i++){
fa[i]=i;
siz[i]=1;
mx[i]=mn[i]=a[sa[i]];
cmx[i]=-inf;
cmn[i]=inf;
}
for(int i=n-1;i>=0;i--)calc(i);
for(int i=0;i<n;i++)printf("%lld %lld\n",ans[i].first,ans[i].second);
return 0;
}