后缀树组 学习笔记

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问题的求解

直接放一些我也不会证的结论:

\[∀1≤i< j < k \le n, lcp(sa_i,sa_k)=min \{lcp(sa_i,sa_j),lcp(sa_j,sa_k) \} \]

\[∀1≤i < j \le n, lcp(sa_i,sa_j)=\min_{k=i+1}^j \{height_k \} \]

\[lcp(sa_i,sa_j) \le lcp(sa_i,sa_k)(i \le j < k) \]

\[∀1 \le i \le n,height_{rk_i} \ge height_{rk_{i-1}}-1 \]

利用最后一个结论,可以快速 \(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\) 的最值,若差比答案大则没有重叠


从这个题里可以引申出另一道的做法:

P2178 [NOI2015] 品酒大会

首先这里的相似即是 \(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;	
}
posted @ 2021-06-24 20:49  y_cx  阅读(81)  评论(1编辑  收藏  举报