后缀数组(SA)学习笔记(倍增算法)

news/2025/3/15 11:43:18/文章来源:https://www.cnblogs.com/bgf0212/p/18773440

后缀数组是一个非常好的东西。一开始看不出来这个东西有什么用,但是它非常的有用。(以下 \(N\) 为字符串长度)
有了后缀数组,我们就可以在 \(O(N \log N)\) 的时间内:

  1. 得到所有后缀的字典序关系。(最基本的功能)
  2. 求出任意两个子串的最长公共前缀 (LCP)。
  3. 求出字符串中本质不同的子串个数(当然,SAM 也可以)。

用 DC3 算法可以达到 \(O(N)\) 的复杂度,但是常数较大。但是本文只介绍倍增的算法。


首先,我们需要知道:

基数排序

非常重要!给一堆字符串排序,我们可以想到先按第一个字符排序,再在有相同第一个字符的一堆字符串里,把它们按第二个字符排序,然后再在前两个字符都相同的一堆字符串里按第三个字符排序……直到排到最后一个字符。

感觉这样非常的麻烦,而且存下来前 \(?\) 个字符相同的所有串复杂度也是非常大的。众所周知,基数排序是稳定的排序,也就是说,排序前后两个相同关键字的元素的相对位置是不会改变的。那么如果我们先给某一个字符排序,再按前一个字符排序,这样排完的顺序就是很对的。至于长度不一样的字符串,只需要从最长的串的最后一位开始排,其它串的这一位看作 \(0\) 就好了。时间复杂度为 \(O(字符串的数量\times 最长的字符串长度)\)
image

实现过程(非常抽象)

对某一位用桶记录各个字符出现的个数,然后求一个前缀和,按照上一次排名结束的顺序,给每个字符串标该字符串对应位置字符的这个数(指前缀和数组),然后这个数减一(因为要让某一位相同的串有不同的排名)。那么如何保证基数排序的稳定性呢,只需要倒着做就好了。(非常抽象)

板(给 n不同的字符串排序,输出最后的顺序):

#include<bits/stdc++.h>
using namespace std;
void write(int x)
{if(x>9)write(x/10);putchar(x%10+'0');
}
const int N=1e5+10;
int n;
char h[N][20];
int rk[N],num[1000],tmp[N],ans[N];
int main()
{scanf("%d",&n);for(int i=1;i<=n;i++)rk[i]=i;//最开始,每个串在它读入时的位置(无所谓)for(int i=1;i<=n;i++)scanf("%s",h[i]+1);for(int k=10;k>=1;k--){for(int i=1;i<=n;i++)tmp[i]=rk[i];//把上一次的顺序转移到另一个数组for(int i=0;i<=300;i++)num[i]=0;//清空桶for(int i=1;i<=n;i++)++num[h[i][k]];//桶for(int i=1;i<=300;i++)num[i]+=num[i-1];//前缀和for(int i=n;i>=1;i--)rk[num[h[tmp[i]][k]]--]=tmp[i];//按顺序放进排序数组}for(int i=1;i<=n;i++)write(rk[i]),putchar(' ');return 0;
}

求后缀数组需要用到基数排序。

倍增求后缀数组,我们用到 \(sa\)\(rk\) 数组。(记从第 \(i\) 位开始的后缀为第 \(i\) 个后缀)

\(sa\) 数组

表示当前排在第 \(i\) 位的是哪一个后缀。

\(rk\)数组

表示当前第 \(i\) 个后缀排在第几位。

不难发现,这两个数组是互逆的。

\(height\) 数组

表示 \(sa_{i-1}\)\(sa_i\) 的 LCP 长度。


实现过程

如果暴力把每个后缀存下来,再一般地排序,时间复杂度 \(O(N^2\log N)\)
如果对每个后缀进行一般的基数排序,时间复杂度 \(O(N^2)\)
发现这些后缀中会有很多相同的片段,考虑每次对所有长度为 \(k\) (或者向后 \(k\) 位超过 \(N\),超过的部分 \(rk\) 视为 \(0\))的子串进行排序,下一次排序把一个长度为 \(2k\) 的子串分成两个长度为 \(k\) 的串,以第一/二个串分别为第一/二关键字进行基数排序。每次排序的时间复杂度为 \(O(N)\),一共排 \(N \log N\)次,总共为 \(O(N\log N)\)

伪代码
for k=1 -> k>=n k每次*2
{对起始下标 1~n 的后缀的第二关键字排序,从 sa 中拿出来,放进 oldsa 数组对第一关键字排序,从 oldsa 数组中拿出来,放进 sa 数组把 rk 数组复制到 oldrk 数组从头开始遍历,如果两个串第一、二关键字都相同,就给它们一样的排名,放进 rk 数组
}

(反正大概就是这么写的)

优化

常数非常大的代码。我们发现有一些事情是没有必要做的:

  1. 对第二关键字排序。因为上一次已经排好序了,所以只需要把第二关键字完全超出原串长度的部分放到最前面,剩下的按照原顺序排列就好了。
  2. 如果每一个后缀的排名都已经不一样,就没有必要继续排序了。
  3. 发现每次为 \(rk\) 重新赋值之后得到一个最大排名,也就是值域,也就是下一轮桶的使用范围。

优化后的伪代码:

for k=1 边界为 p<n k每次*2
{把第二关键字完全超出字符串长度的后缀放到最前面,其他的按照原顺序排列,放进 oldsa对第一关键字排序,从 oldsa 数组中拿出来,放进 sa 数组交换 oldrk 和 rk 数组的指针从头开始遍历,如果两个串第一、二关键字都相同,就给它们一样的排名,放进 rk 数组,用 p 记录排名
}

板:

#include<bits/stdc++.h>
using namespace std;
void write(int x)
{if(x>9)write(x/10);putchar(x%10+'0');
}
const int N=1e6+10;
char h[N];
int sa[N*2],oldsa[N],num[N],rk[N*2],oldrk[N];
int n,m=300,p;
int main()
{scanf("%s",h+1);n=strlen(h+1);for(int i=1;i<=n;i++)++num[rk[i]=h[i]];//基数排序这里也可以写成一个单独的函数for(int i=1;i<=m;i++)num[i]+=num[i-1];for(int i=n;i;i--)sa[num[rk[i]]--]=i;for(int k=1;p<n;k<<=1,m=p)//m 为值域{int now=0;for(int i=n-k+1;i<=n;i++)oldsa[++now]=i;//把超出的部分放到最前for(int i=1;i<=n;i++)if(sa[i]>k)oldsa[++now]=sa[i]-k;//其余原顺序放入memset(num,0,(m+1)*sizeof(int));for(int i=1;i<=n;i++)++num[rk[i]];for(int i=1;i<=m;i++)num[i]+=num[i-1];for(int i=n;i;i--)sa[num[rk[oldsa[i]]]--]=oldsa[i];//对第二关键字基数排序,注意这一行要倒序进行swap(rk,oldrk);//交换指针,相当于复制过去p=0;for(int i=1;i<=n;i++)//重新排序if(oldrk[sa[i]]==oldrk[sa[i-1]]&&oldrk[sa[i]+k]==oldrk[sa[i-1]+k])rk[sa[i]]=p;else rk[sa[i]]=++p;}for(int i=1;i<=n;i++)write(sa[i]),putchar(' ');return 0;
}

\(height\) 数组

有定理:\(height_{rk_{i}}\ge height_{rk_{i-1}}\),证明略。
时间复杂度为 \(O(N)\),证明也略。

板:

	for(int i=1,k=0;i<=n;i++){if(k)--k;while(s[i+k]==s[sa[rk[i]-1]+k])++k;he[rk[i]]=k;}

一些其他功能

求任意两个后缀的 LCP

要求后缀 \(i\)\(j\) 的 LCP(不妨设 \(rk_i < rk_j\)),只需求出 \(height\) 数组中下标在 \([rk_i,rk_j]\) 的最小值。用 st 表维护即可。

求字符串中本质不同的子串个数

设结果为 \(ans\),有结论:\(ans=\Sigma_{i=1}^N N-sa_i-height_i+1=N\times (N+1)-\Sigma_{i=1}^N height_i\)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/899103.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Chat2DB 数据库客户端邀请码

推荐一款超好用的AI数据库管理工具Chat2DB,支持22种数据库,包括国产的达梦、OpenGuass、OceanBase、TiDB等,还有非关系型MongoDB、Redis等,快来试试吧! 🔥 官网:https://chat2db-ai.com/ 🔥 邀请码:622888 邀请码介绍:https://docs.chat2db-ai.com/docs/settings/i…

基于Java的全栈入门学习路线

Java全栈JavaSE数据库前端JavaWebSSM框架Linux学完以上可独立开发,下面是微服务协同开发 SpringBootSpringCloudHadoop

latex中如何重复引用已经存在的脚注?

在使用latex写论文的时候,如果要添加一个脚注,可以使用 \footnote{...}如果第二次出现相同内容的脚注,再写一次\footnote{...}会导致出现两个脚注,我们希望两处脚注链接到一处,可以这样写: 第一处脚注添加label: \footnote{...\label{llama3.2}}第二处及之后的脚注直接引…

编译libssh (Windows VS)

1、先编译依赖的zlib 下载地址 zlib Home Site 解压后,文件夹处——鼠标右键——VS Code打开,选择vs编译器,开始自动编译得到build目录 以管理员身份运行VS,打开build目录里的zlib.sln,我需要的是64位的,所以选择如下 ALL_BUILD处生成lib和dll,然后INSTALL处也生成下(自…

【学习笔记】wqs二分

其实写这个主要是想解释一下它的原理,教程、习题什么的网上都有,比如这个。 就拿这题来讲吧。 首先我们画出一个函数 \(f(x)\) 表示 \(s\) 的度恰好为 \(x\) 时,最小生成树的权值和。 当然,这个函数只会取在某一些整点上,我们把它连起来就行了。 然后你会发现它是下凸的(…

在vue2中引用高德地图,外卖骑手的路线规划

参照路径规划-参考手册-地图 JS API 1.4 | 高德地图APIAMap.RidingAMap.Riding骑行路径规划服务,提供起始、终点骑行路线查询服务。用户可以通过自定义回调函数取回并显示查询结果。若服务请求失败,系统将返回错误信息。 相关示例:位置经纬度 + 骑行路线规划-骑行路径规划-示…

软件分析——OBS Studio

课程社区链接:首页 - 2025年春季软件工程(罗杰、任健) - 北京航空航天大学 - 班级博客 - 博客园 (cnblogs.com) 作业要求链接:[I.2] 个人作业:软件案例分析 - 作业 - 2025年春季软件工程(罗杰、任健) - 班级博客 - 博客园 (cnblogs.com) 课程目标:让学生掌握软件开发的…

JDK7-时间类、时间格式化类--java进阶day07

1.Date类:表示时间的类 1.Date常用的构造方法.2.Date常用的成员方法1.getTime:返回从时间原点到对象设定的时间之间的时间2.setTime:将对象的时间设置为setTime里的时间 d3是此刻的时间,被setTime设置为了时间原点2.SimpleDateFormat类:时间格式化类 Date类默认的格式不好看…

RN里遇到初始计算值是一样的,布局位置却不一致的问题

在tabs切换下面的小动画 但在页面上显示确是这样的,只有切换回来一次后才显示正常。计算结果确始终都是7 问了下AI,说是,在RN中,布局计算是异步的。在组件首次渲染时,即使布局事件已经触发并且获取到了layout对象,实际DOM可能还没有完全应用这些尺寸。这会导致第一次动画…

idea安装激活图文详细教程_激活至永久_亲测有效

一、安装 IDEA打开下载好的安装包,按照提示完成安装。 image.png自定义安装路径,我这里安装在了 E:\ 盘下,继续点击下一步按钮: image.png image.png安装完成后,暂时不要启动程序,直接关闭它。三、补丁安装步骤 1. 下载补丁 点击以下链接获取补丁:点击获取补丁文件。htt…

『Plotly实战指南』--绘图初体验

今天,打算通过绘制一个简单的散点图,来开启我们 Plotly 绘图的初次尝试。 本文目的不是介绍如何绘制散点图,而是通过散点图来介绍Plotly 绘图的基础步骤。 1. 绘制散点图:初探 Plotly 散点图是展示变量关系的基础图表,在Plotly中,绘制散点图非常简单。 以下是一个示例代码…