SA后缀数组

devtools/2024/10/21 23:08:50/

基础概念:

子串:

在一个字符串s中,取任意i<=j,那么从i到j的这一段就叫做s的一个子串
后缀:

从字符串s的某个位置i到字符串末尾的子串,
suff[i]:

以s的第i个字符为第一个元素的后缀
后缀数组:
现有字符串s,长度为n
把s的全部后缀按照字典序排序,总共有n个后缀
sa[i]:

表示排名为i的后缀的起始位置的下标——根据排名来查询后缀起始位置
rak[i]:

表示起始位置为i的后缀的排名——根据起始位置来查询后缀排名

LCP:
LCP[i][j]:表示suff[sa[i]](排名为i的后缀)与suff[sa[j]](排名为j的后缀)的最长公共前缀的长度
LCP[i][j]=LCP[j][j]
LCP[i][i]=Len[sa[i]]=n-sa[i]+1;
该式子的意思为,自身与自身的最长公共前缀就是自身,其长度为排名为i的后缀的长度,而其长度等于n减去起始位置的下标再加1
LCP性质:
当i<j<k时
LCP[i][j]>=LCP[i][k]
显然由字典序排序的单调性可知,离i越近的后缀与i的前缀重合度就会越大,即长度越长

Height[i]:

表示LCP[i][i-1],即排名为i的后缀与排名为i-1的后缀的最长公共前缀

后缀数组实现:DC3 O(n)
                         DA O(nlogn)

倍增实现:

倍增思路:
对以每个字符为开头的长度为2^k的子字符串进行排序,即为rank值
k从0开始,每次+1
当2^k的长度大于n以后
以第一个字符开头的长度为2^k的子字符串已经包含了整个字符串
所以每个字符开始的长度为2^k的子字符串就相当于所有的后缀
对于靠后的起始字符的子串而言,若其到字符串末尾的长度小于2^k,那么就取它的实际长度
并且我们对这些子字符串都已经用一定的方法比较出了大小
即rank值里面没有相同的值
那么此时的rank值就是最后的结果

那么如何得到这个rank值呢?
我们利用倍增的思想
我们对于n个长度为2^k的子字符串进行rank值排序
当k等于0的时候,子串长度为1,直接按照字典序排出rank值大小即可
假设我们要比较a和b两个长度为2^k的子字符串
只需要将其划分成为两个2^(k-1)长度的子字符串
即,将a划分为a1和a2,将b划分为b1和b2,长度均为2^(k-1)
那么我们先比较a1和b1的rank值大小
若rank[a1]!=rank[a2],则已经比较出a和b的字典序大小
若rank[a1]==rank[a2],我们再进而比较a2和b2的rank值
由于两个子串rank值不可能相同,则一定能够通过rank[a2]和rank[b2]比较出a和b的大小
这样,当2^k>n时,我们就得到了sa数组

后缀数组的应用举例:

后缀数组为排序后的数组,具有一定的单调性

求无重复子串:

对于一个长度为n的字符串而言,其最多有n*(n+1)/2个不重复子串
思考过程:
对于每一个子串,我们可以分其以不同的起始位置讨论
以下标为1的开头的子串有n个
以下标为2的开头的子串有n-1个
以此类推
最后就是一个1+2+3+……+n的求和
故有n*(n+1)/2个子串
对于s中的每一个子串
一定存在于每一个后缀的前缀中
即每一个后缀的前缀都会是一个子串
那么我们只需要将重复的前缀剔除,剩余的后缀所贡献的前缀的总和就是无重复子串的个数
由于字典序排序问题
相邻的两个后缀的前缀重合度一定是最大的

即对于排名为i的后缀而言
其能贡献的子串个数为n-sa[i]+1-Height[i]

求可重叠的k次最长重复子串
由上面第一道题的讨论已知
每一个子串一定存在于每一个后缀的前缀中
那么要求可重复的子串
就是求后缀数组中可重复的前缀
这就是height数组的含义

由于按字典序排序后
后缀的前缀重叠度最大
当i<j<k时
LCP[i][j]>=LCP[i][k]
因此选一个连续的区间的前缀
他们的可重叠度是最大的
单调性——二分——枚举最长长度

模版代码:

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 100010;//将待排序的字符串放到r数组中
//从r[0]到r[n-1],长度为n
//且最大值小于<M
//并约定除了r[n-1]外的r[i]都大于0,且r[n-1]=0
//函数结束后,再把结果放到sa数组中,范围从sa[0]到sa[n-1]
int wa[N], wb[N], wv[N], wss[N], cal[N], n, sa[N];
char s[N];
int cmp(int *r, int a, int b, int l)
{return r[a] == r[b] && r[a + l] == r[b + l];
}
void da(int *r, int *sa, int n, int M)
{int i, j, p, *x = wa, *y = wb, *t;//由于r[i]的最大值不会很大,所以这里采用基数排序//如果r[i]的最大值很大,可以改为快速排序//基数排序for (i = 0; i < M; i++)wss[i] = 0;//令计数初值为0for (i = 0; i < n; i++)wss[x[i] = r[i]]++;//给x数组赋初值为r,并令r[i]对应的wss加1for (i = 1; i < M; i++)wss[i] += wss[i - 1];//前缀和,求出r[i]的字母前共有几个字母for (i = n - 1; i >= 0; i--)sa[--wss[x[i]]] = i;//wss[x[i]]就表示是x[i]前有几个字母,同时也是x[i]字母的排名+1//那么sa[--wss[x[i]]]就是x[i]也就是r[i]的排名所对应的起始字母的位置,也就是i//若干次基数排序//j为当前处理子串长度//数组y保存对第二关键字排序的结果//也就是y[i]表示第二关键字排名为i的起始字母的位置//然后对第一关键字排序for (j = 1, p = 1; p < n; j *= 2, M = p){//第一次基数排序//对第二关键字排序for (p = 0, i = n - j; i < n; i++)//i会加j次,p也会加到jy[p++] = i;for (i = 0; i < n; i++)//总共会有n-j个y满足sa[i]>=j//加上前面有j个y//y数组总共的长度为n//当排名为i的子串的起始位置大于等于j的时候//说明他会被前面的长度为j的子串接上,也就是说它有可能成为第二关键值,因此需要求出他的第二关键值的排名//又由于长度小于j的子串的y分别被初始化为n-j,n-j+1......n-1//那么长度大于等于j的子串的起始位置再重新记作0,1,2,3......//这样一来,对于上次得到的sa的排名来说//长度小于j的子串从第0排到第j-1//长度大于等于j的子串的相对排名不变,且连续在一起,分别从第n-j-1排到第n-1if (sa[i] >= j)y[p++] = sa[i] - j;//y数组保存对第二关键字排序的结果//第二次基数排序//对第一关键字排序for (i = 0; i < n; i++)wv[i] = x[y[i]];//wv[i]表示第二关键值排名为i的字母的关键字的值(即第一关键字的值)for (i = 0; i < M; i++)wss[i] = 0;for (i = 0; i < n; i++)wss[wv[i]]++;for (i = 1; i < M; i++)wss[i] += wss[i - 1];for (i = n - 1; i >= 0; i--)sa[--wss[wv[i]]] = y[i];//排名为--wss[wv[i]]的后缀的起始位置为y[i]//计算rank数组//更新rank值//首先交换x数组和y数组//更新排名从0到n-1的rank//由于rank会有重复的,所以要比较一下sa[i-1]和sa[i]的rank是否相同//相同的话,x[sa[i]]和x[sa[i-1]]的值就应该相同,所以取p-1//否则的话不同,取p,且p递增for (t = x, x = y, y = t, p = 1, x[sa[0]] = 0, i = 1; i < n; i++)x[sa[i]] = cmp(y, sa[i - 1], sa[i], j) ? p - 1 : p++;}return;
}int rak[N], height[N];
// 获取height数组
void calheight(int *r, int *sa, int n)
{int i, j, k = 0;//先初始化rank数组for (i = 1; i <= n; i++) rak[sa[i]] = i;//k为最长公共前缀长度//初始化为0//这里的O(n)算法运用到性质height[i]>=height[i-1]-1for (i = 0; i < n; height[rak[i++]] = k) for (k ? k-- : 0, j = sa[rak[i] - 1]; r[i + k] == r[j + k]; k++);// k是否存在?// k存在就递减1// j等于以第i个字母起始的后缀的前一个排名的后缀的起始字母的位置// 如果以第i个字母往后第k个也和以第j个字母往后第k个相同,那么k++,继续循环,直到找到不同的字母// 那么k就是sa[rak[i]]与sa[rak[i]-1]的最长公共前缀的长度// 最后将height[rak[i]]的值更新为k(也就是外层循环的for的第三个语句)for (int i = n; i; i--) rak[i] = rak[i - 1], sa[i]++;
}
int main()
{int cas = 1;while (~scanf("%s", s + 1)){n = strlen(s + 1);for (int i = 1; i <= n; i++)cal[i] = s[i];cal[n + 1] = 0;da(cal + 1, sa, n + 1, 200);calheight(cal + 1, sa, n);for (int i = 1; i <= n; i++){printf("%d ", height[i]);}puts("");for (int i = 1; i <= n; i++){printf("%d ", sa[i]);}puts("");for (int i = 1; i <= n; i++){printf("%d ", rak[i]);}puts("");}
}

http://www.ppmy.cn/devtools/87674.html

相关文章

Android 10.0 Launcher 启动流程

在前面SystemUI启动流程中说到&#xff0c;在SystemServer中会去启动各种系统服务&#xff0c;这里的launcher也是启动的其中一个服务ActivityManagerService去启动的。在android10之前&#xff0c;系统四大组件的启动都是在ActivityManagerService中&#xff0c;在android10中…

组蛋白乳酸化和RNA甲基化如何联动?请大数据把这个思路推给科研人

在细胞生物学中&#xff0c;基因表达调控是决定细胞功能与命运的核心过程之一。组蛋白作为修饰性蛋白&#xff0c;在调控基因转录中起着至关重要的作用。近年来&#xff0c;科学家们发现&#xff0c;组蛋白的多种化学修饰&#xff08;如甲基化、乙酰化、磷酸化等&#xff09;影…

pycharm上设置直接点击run使用pytest,而不需要执行main

1.设置默认执行框架 file-settings-Tools-Python Integrated Tools-找Testing-设置Default test runner 为pytest 2.进入 Edit Configurations,用- 删除python下的文件&#xff0c;点击apply–>ok 即可 3.不用写main就可以直接运行测试用例

嵌入式人工智能(39-基于树莓派4B的震动传感器和霍尔传感器)

这两个传感器实验比较简单&#xff0c;也都属于力传感器&#xff0c;就放一起做了。 1、震动传感器 震动传感器是一种用于检测和测量物体震动、振动和冲击的设备。它通常由一个敏感元件和一个信号处理单元组成。敏感元件可以是压电材料、光电材料、加速度传感器等。当物体发生…

Leaflet封装以及扩展自定义插件教程

从事leaflet开发的同学或多或少应该都接触过leaflet的插件,因为leaflet的框架理念是只封装最基础最核心的部分,因此优点在于代码体积非常小,使用起来简单轻便。同时缺点就是功能组件没那么丰富,有很多开发需求都必须借助第三方插件来完成。因此你可以看到leaflet的插件市场…

如何将整个运行环境打包成docker

场景 某个项目&#xff0c;用的tomcatrediszookeeper&#xff0c;然后这个项目已经产品化&#xff0c;很多地方都需要部署&#xff0c;并且有很多有细微差别的版本。 然后我这边是需要部署测试环境&#xff0c;一台机可能会部署好几个。 按照传统部署方式&#xff0c;要好几个…

(39)智能电池

文章目录 前言 1 通过任务规划器进行设置 2 补充信息 3 限制条件 4 参数说明 前言 虽然还不是很普遍&#xff0c;但智能电池更容易从飞行器上安装和拆卸&#xff0c;并且能够提供更多关于电池状态的信息&#xff0c;包括容量、单个电池电压、温度等。 ArduPilot 支持几种…

Java面试八股之@Qualifier的作用

Qualifier的作用 Qualifier 是 Spring 框架中的一个非常有用的注解&#xff0c;它主要用于解决在依赖注入过程中出现的歧义问题。当 Spring 容器中有多个相同类型的 Bean 时&#xff0c;Qualifier 可以帮助指明应该使用哪一个具体的 Bean 进行注入。 Qualifier 的作用&#x…