SA后缀数组

ops/2024/9/24 1:16:06/

基础概念:

子串:

在一个字符串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/ops/87352.html

相关文章

Java:多线程(进程线程、线程状态、创建线程、线程操作)

1&#xff0c;线程概述 1.1&#xff0c;进程和线程 并发是指系统能够同时处理多个任务或操作&#xff0c;通常通过在单个处理器或多个处理器之间快速切换上下文来实现。这些任务可能不是同时进行的&#xff0c;但是它们在时间上重叠。 并行是指系统同时执行多个任务或操作&…

分布式锁详解

文章目录 分布式锁基于数据库的分布式锁基于Redis的分布式锁基于Zookeeper的分布式锁基于Consul的分布式锁 分布式锁 在单机程序中&#xff0c;我们常用ReetrantLock、synchronized保证线程安全。类似这样&#xff1a; public class MainTest {private static final Reentra…

Linux-3:Shell编程——基础语法(0-50%)

目录 前言 一、变量 1.定义变量 2.使用变量 3.修改变量 4.将命令的结果赋值给变量 5.只读变量 6.删除变量 二、传递参数 三、字符串 1.字符串举例 2.统计字符串长度 3.字符串拼接 4.截取字符串 总结 前言 Shell是一种程序设计语言。作为命令语言&#xff0c;它…

一篇文章带你入门爬虫并编写自己的第一个爬虫程序

一、引言 目前我们处在一个信息快速迭代更新的时代&#xff0c;海量的数据以大爆炸的形式出现在网络之中&#xff0c;相比起过去那个通过广播无线电、书籍报刊等传统媒介获取信息的方式&#xff0c;我们现在通过网络使用搜索引擎几乎可以获得任何我们需要的信息资源。 但与此同…

JMeter接口测试-5.JMeter高级使用

JMeter高级使用 案例&#xff1a; 用户登录后-选择商品-添加购物车-创建订单-验证结果 问题&#xff1a; JMeter测试中&#xff0c;验证结果使用断言&#xff0c;但断言都是固定的内容假如要判断的内容(预期内容)是在变化的, 有时候还是不确定的, 那该怎么办呢? 解决&…

应急靶场(11):【玄机】日志分析-apache日志分析

题目 提交当天访问次数最多的IP&#xff0c;即黑客IP黑客使用的浏览器指纹是什么&#xff0c;提交指纹的md5查看index.php页面被访问的次数&#xff0c;提交次数查看黑客IP访问了多少次&#xff0c;提交次数查看2023年8月03日8时这一个小时内有多少IP访问&#xff0c;提交次数 …

图方法与机器学习实战:从理论到应用的全景指南

《动手学图机器学习》并不是一本纯粹介绍图机器学习理论的著作&#xff0c;Alessandro Negro 博士作为科学家和 Reco4 公司的 CEO&#xff0c;长期维护图数据源的推荐系统。他结合机器学习工程和图机器学习方法&#xff0c;通过推荐引擎、欺诈检测和知识图谱等案例&#xff0c;…

CSP:内容安全策略的前端深入解析

CSP&#xff1a;内容安全策略的前端深入解析 在当今的网络安全环境中&#xff0c;内容安全策略&#xff08;Content Security Policy&#xff0c;简称CSP&#xff09;是一种至关重要的安全机制。作为前端开发专家&#xff0c;深入了解并合理应用CSP&#xff0c;对于提升Web应用…