浅谈后缀数组

字符串科技还是躲不开的呢😅。
本博客主要讲和后缀数组与后缀排序有关的知识点和题目。
后缀数组,顾名思义就是存储与字符串的一系列后缀有关内容的数组,有的时候,我们需要处理关于字符串字串的问题,就可以通过将其转化为后缀的前缀来解决。
后缀数组主要有两个: sa[i] 表示所有后缀中第 i 小的后缀编号, rk[i] 表示编号为 i 的后缀在所有后缀中的排名。
显然可以得到 sa[rk[i]]=irk[sa[i]]=i

后缀排序

那么最关键的就是如何进行后缀排序。
我们直接暴力比较字符串是不现实的,因为单次字符串比较是 O(n) 的,而基于比较的排序算法是 Ω(nlogn) 的,所以最有情况也是 O(n2logn) 的,显然不显示。
于是我们考虑使用一些非暴力比较的排序方法——一种类似基数排序的思想。

我们记 s(i,j) 表示字符串从第 i 为开始的,长度为 j 的字符串(原字符串长度不够则补 '\0' ),对于所有后缀的排序就可以转化为对 s(i,k) 的排序,其中 kn
假设我们将所有 s(i,w) 的排序,我们尝试将 s(i,2w) 排序,由于 s(i,2w)=s(i,w)+s(i+w,w) ,也就是说我们新的需要排序的字符串是有我们已知顺序的字符串组成的,这也就意味着我们分别用两个关键字进行排序就可以得到它的顺序,而这个过程是 O(n)
最终我们得到 s(i,2logn) 的顺序了,考虑起始情况: s(i,1)=ci ,顺序即为字符大小。
这样我们就可以做到 O(nlogn) 排序了,而且常数较小。
代码实现:

m=max(n,300);
for(i=1;i<=n;i++)
	sa[i]=i,rk[i]=s[i];
for(i=1;i<=n;i++) ++cnt[rk[i]=s[i]];
for(i=1;i<=m;i++) cnt[i]+=cnt[i-1];
for(i=n;i>=1;i--) sa[cnt[rk[i]]--]=i;
for(w=1;w<n;w<<=1)
{
	memset(cnt,0,sizeof(cnt));
	for(i=1;i<=n;i++) id[i]=sa[i];
	for(i=1;i<=n;i++) ++cnt[rk[id[i]+w]];
	for(i=1;i<=m;i++) cnt[i]+=cnt[i-1];
	for(i=n;i>=1;i--) sa[cnt[rk[id[i]+w]]--]=id[i];
	memset(cnt,0,sizeof(cnt));
	for(i=1;i<=n;i++) id[i]=sa[i];
	for(i=1;i<=n;i++) ++cnt[rk[id[i]]];
	for(i=1;i<=m;i++) cnt[i]+=cnt[i-1];
	for(i=n;i>=1;i--) sa[cnt[rk[id[i]]]--]=id[i];
	memcpy(rk_,rk,sizeof(rk));
	for(p=0,i=1;i<=n;i++)
		{
		if(rk_[sa[i]]!=rk_[sa[i-1]]||rk_[sa[i]+w]!=rk_[sa[i-1]+w]) p++;
		rk[sa[i]]=p;
	}
}
for(i=1;i<=n;i++) rk[sa[i]]=i;

Luogu 【模板】后缀排序

后缀排序例题

[NOI2015] 品酒大会

我们统计一个数对的贡献是先转化成最大的“ r 相似”,最后将答案取后缀和或 max 即可。
题意需要求原串的两个后缀 si,sj 的前缀长度。
首先进行后缀排序,我们设排完序之后的数组中,第 i 项和第 j 项的最长公共前缀为 LCPi
不难发现,后缀数组中第 l 项和第 r 项的最长公共前缀长度为 mini=lr1LCPi ,因为数组进行了后缀排序,所以 l 项和 r 项的最长公共前缀也必然是 lr 项的最长公共前缀。
使用 O(n)LCPi 即可(其实这个叫 height 数组)。
使用分治每次找到 [L,R] 内最小的 LCPi ,然后对两侧进行递归,复杂度 O(nlogn)

[NOI2016] 优秀的拆分

题意可以转化为求从每个位置有多少个开始或结束的形如 AA 的串。
如果上式中 A 串的长度为 len ,在原串上每间隔 len 标一个带点,则 AA 必然经过两点,而且这两个点会处在两个 A 串中完全相同的位置。
所以考虑维护出来每个前缀的最长公共后缀和后缀的最长公共前缀,枚举相邻两点的 LCP 和 LCS ,如果 LCP+LCSlen 才对长为 lenAA 串做贡献,具体贡献范围可以计算得出。
整体复杂度为 O(nlogn)

[JSOI2007] 字符加密

题目可以转化为对 SS 的前 n 项进行排序,后缀排序即可,总复杂度为 O(nlogn)

树上后缀排序

对于一个有根树,每一个节点都有一个字符,有时候我们需要处理和由根和某节点之间路径构成字符串有关的问题,考虑将普通后缀排序拓展到树上。
一般这种题目要求以从节点到根的字符串为第一关键字,以从根到节点的编号串为第二关键字。
我们难免会遇到在正常字符串题目中不会遇到的问题:字符串重复。因为一个字符串的后缀长度都不相同,不会出现重复的问题,但是树上不一样,
考虑我们倍增排序过程中的优先级:

  1. 节点字符串长度
  2. 祖先字符串长度
  3. 祖先编号
  4. 节点编号
    我们将 2,3 合并,改为一个不重复的排名数组即可。
    代码实现:
for(int i=1;i<=n;i++) sa[i]=i,rk[i]=a[i];
for(int i=1;i<=n;i++) cnt[rk[i]]++;
for(int i=1;i<=n;i++) cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--) sa[cnt[rk[i]]--]=i;
for(int i=1;i<=n;i++) rk2[sa[i]]=i;
for(int w=1;w<n;w<<=1)
{
	memset(cnt,0,sizeof(cnt));
	for(int i=1;i<=n;i++) id[i]=sa[i];
	for(int i=1;i<=n;i++) cnt[rk2[fa[id[i]]]]++;
	for(int i=1;i<=n;i++) cnt[i]+=cnt[i-1];
	for(int i=n;i>=1;i--) sa[cnt[rk2[fa[id[i]]]]--]=id[i];
	memset(cnt,0,sizeof(cnt));
	for(int i=1;i<=n;i++) id[i]=sa[i];
	for(int i=1;i<=n;i++) cnt[rk[id[i]]]++;
	for(int i=1;i<=n;i++) cnt[i]+=cnt[i-1];
	for(int i=n;i>=1;i--) sa[cnt[rk[id[i]]]--]=id[i];
	memcpy(rk_,rk,sizeof(rk));
	for(int p=0,i=1;i<=n;i++)
	{
		if(rk_[sa[i]]!=rk_[sa[i-1]]||rk_[fa[sa[i]]]!=rk_[fa[sa[i-1]]]) p++;
		rk[sa[i]]=p,rk2[sa[i]]=i;
	}
	for(int i=n;i>=1;i--) fa[i]=fa[fa[i]];
}
for(int i=1;i<=n;i++) rk[sa[i]]=i;

Luogu 树上后缀排序

树上后缀排序例题

Luogu 【XR-1】柯南家族

发现它讲了一大坨比较方式,讲的就是进行树上后缀排序之后编号反转即可。
然后需要维护的就是到根链上第 k 小和子树内第 k 小,使用线段树很容易维护。
总体复杂度为 O(nlogn)

CF207C3 Game with Two Trees

发现这道题需要匹配的两棵树的字符串是颠倒的,必然只能将其中一个以Trie树形式维护,另一个维护树上后缀数组即可。
发现 T1 中的字符串必然是完整的从节点到根,所以考虑维护 T2 的树上后缀数组。
由于在线处理加点不好维护,所以考虑离线下来先处理出来后缀数组再统计贡献。
观察性质,设 T1 中对 u 节点做贡献的 T2 中节点排名为 [Lu,Ru] ,由于 fau 的串为 u 的串的前缀,所以 [Lu,Ru][Lfau,Rfau] ,同时 u 的串只多出了1位,所以只需要二分查找比较 T2[Lfau,Rfau] 中串的某一位即可,反应在树上为某节点的 k 级祖先,因为要支持 O(nlogn) 次查找,使用 O(nlogn)O(1)O(nlogn)O(logn) 的查找均可。

posted @   Xun_Xiaoyao  阅读(49)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
/* 鼠标点击求赞文字特效 */
点击右上角即可分享
微信分享提示