浅谈后缀数组
字符串科技还是躲不开的呢😅。
本博客主要讲和后缀数组与后缀排序有关的知识点和题目。
后缀数组,顾名思义就是存储与字符串的一系列后缀有关内容的数组,有的时候,我们需要处理关于字符串字串的问题,就可以通过将其转化为后缀的前缀来解决。
后缀数组主要有两个: sa[i]
表示所有后缀中第 i
小的后缀编号, rk[i]
表示编号为 i
的后缀在所有后缀中的排名。
显然可以得到 sa[rk[i]]=i
, rk[sa[i]]=i
。
后缀排序
那么最关键的就是如何进行后缀排序。
我们直接暴力比较字符串是不现实的,因为单次字符串比较是
于是我们考虑使用一些非暴力比较的排序方法——一种类似基数排序的思想。
我们记 '\0'
),对于所有后缀的排序就可以转化为对
假设我们将所有
最终我们得到
这样我们就可以做到
代码实现:
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;
后缀排序例题
[NOI2015] 品酒大会
我们统计一个数对的贡献是先转化成最大的“
题意需要求原串的两个后缀
首先进行后缀排序,我们设排完序之后的数组中,第
不难发现,后缀数组中第
使用 height
数组)。
使用分治每次找到
[NOI2016] 优秀的拆分
题意可以转化为求从每个位置有多少个开始或结束的形如
如果上式中
所以考虑维护出来每个前缀的最长公共后缀和后缀的最长公共前缀,枚举相邻两点的 LCP 和 LCS ,如果
整体复杂度为
[JSOI2007] 字符加密
题目可以转化为对
树上后缀排序
对于一个有根树,每一个节点都有一个字符,有时候我们需要处理和由根和某节点之间路径构成字符串有关的问题,考虑将普通后缀排序拓展到树上。
一般这种题目要求以从节点到根的字符串为第一关键字,以从根到节点的编号串为第二关键字。
我们难免会遇到在正常字符串题目中不会遇到的问题:字符串重复。因为一个字符串的后缀长度都不相同,不会出现重复的问题,但是树上不一样,
考虑我们倍增排序过程中的优先级:
- 节点字符串长度
- 祖先字符串长度
- 祖先编号
- 节点编号
我们将 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 【XR-1】柯南家族
发现它讲了一大坨比较方式,讲的就是进行树上后缀排序之后编号反转即可。
然后需要维护的就是到根链上第
总体复杂度为
CF207C3 Game with Two Trees
发现这道题需要匹配的两棵树的字符串是颠倒的,必然只能将其中一个以Trie树形式维护,另一个维护树上后缀数组即可。
发现
由于在线处理加点不好维护,所以考虑离线下来先处理出来后缀数组再统计贡献。
观察性质,设
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧