首页 > 其他分享 >后缀数组

后缀数组

时间:2024-08-20 10:37:46浏览次数:14  
标签:后缀 字母 数组 sa 排序 2k

先介绍计数排序。思考一下桶排序,桶排序是不稳定的。计数排序相当于是稳定的桶排序,时间复杂度为\(O(值域)\)

设数组\(a\)的值域为\([1,n]\),数组\(c\)表示每个元素的数目(也就是桶),数组\(r[i]\)表示\(a[i]\)的排名(注意这个排名是稳定的,也就是说当有多个\(a[i]\)的时候,相对顺序不会变化),然后对\(c\)求前缀和,这样\(c[i]\)就表示小于等于\(i\)的数有多少个(下面的\(c\)都表示已经求了前缀和的\(c\))。然后倒序循环数组\(a\),循环到\(a[i]\)的时候,令\(r[i]=c[a[i]]\),并令\(c[a[i]]\)减一

正确性:假设有若干个\(k\),我们现在只关心\(k\)。那么我们知道,最终排完序的\(a\)数组,严格小于\(k\)的有\(c[k-1]\)个,也就是说\(k\)的排名最低是\(c[k-1]+1\),最高是\(c[k]\);而由于要稳定,所以原数组\(a\)中最后一个\(k\)的排名是\(c[k]\),最前面的一个\(k\)的排名是\(c[k-1]+1\);我们倒序循环,由于\(c\)求了前缀和,上述过程刚好帮我们完成了排序工作

接下来进入正题,介绍后缀数组。下文默认字符串下标从\(1\)开始,那么长度为\(n\)的字符串\(s\)一共有\(n\)个后缀,第\(i\)个后缀就是\(s[i...n]\)

后缀数组涉及三个数组:\(sa,rk,height\)。\(sa[i]\)表示\(s\)所有后缀中,排名为\(i\)的后缀是第几个后缀;\(rk[i]\)表示\(s\)的第\(i\)个后缀的排名;\(height[i]\)表示排名第\(i\)位的后缀与排名第\(i-1\)位的后缀的最长公共前缀(\(\text{lcp}\))

然后先求\(sa\)。下文的\(x[i]\)表示按照前\(k\)个字母排序的前提下,\(s\)的第\(i\)个后缀的离散化之后的值(若不同后缀的前\(k\)个字母相同,则离散化之后的值相同),\(m\)表示离散化值域,\(y[i]\)数组表示排名为\(i\)的是第几个后缀(最后也会作为辅助数组)

第一步:将所有后缀按照第一个字母稳定地排序(利用计数排序)

for(int i=1;i<=n;i++) c[x[i]=s[i]]++;
//这里由于字符集不大,所以桶的大小直接开成字符集大小,离散化按照字符离散化
for(int i=2;i<=m;i++) c[i]+=c[i-1];
for(int i=n;i;i--) sa[c[x[i]]--]=i;

第二步:假设我们已经对所有后缀按照前\(k\)个字母稳定地排好了序,我们接下来将所有后缀按照前\(2k\)个字母稳定地排序。

外层循环:for (int k = 1; k <= n; k <<= 1)

我们先按照所有后缀的第\(k+1\) ~ \(2k\)个字母排序(这里不要求稳定),然后再按照所有后缀的第\(1\) ~ \(k\)个字母排序(这里要求稳定);由于不可能存在两个后缀相同,所以经过上面两次排序之后,得到的\(sa\)一定是按所有后缀按照前\(2k\)个字母稳定地排序得到的

先进行第一次排序:

对于\(\forall i∈[n-k+1,n],s[i...n]\)是没有前\(2k\)个字母的,由于空字符串的字典序小于任何字符串,所以我们直接按照顺序将其放入\(y\)中即可

int num = 0;
for (int i = n - k + 1; i <= n; i ++ ) y[ ++ num] = i;

对于\(i∈[1,n-k],s[i...n]\)的第\(k+1\) ~ \(2k\)个字母就是\(s[i+k...i+2k]\)(长度不够的用空字符串补齐),于是比较\(s[i...n]\)的第\(k+1\) ~ \(2k\)个字母就是比较\(s[i+k...i+2k]\),也就是\(s\)的第\(i+k\)个后缀的前\(k\)个字母,而此时我们的\(sa[i]\)刚好记录了按照前\(k\)个字母排序,排名为\(i\)的后缀,于是我们从小到大循环\(i\),若\(sa[i]>k\),则将\(sa[i]-k\)放入\(y\)数组即可

for (int i = 1; i <= n; i ++ )
if (sa[i] > k) y[ ++ num] = sa[i] - k;

再进行第二次排序:

首先按照前\(k\)个字母进行计数排序

for (int i = 1; i <= m; i ++ ) c[i] = 0;
for (int i = 1; i <= n; i ++ ) c[x[i]] ++ ;
for (int i = 2; i <= m; i ++ ) c[i] += c[i - 1];
for (int i = n; i; i -- ) sa[c[x[y[i]]] -- ] = y[i];

然后将\(y\)作为辅助数组记录所有后缀按照前\(k\)个字母排序的离散化数值

swap(x, y);

然后更新\(x\),让\(x[i]\)表示s的第\(i\)个后缀按照前\(2k\)个字母排序的离散化数值(注意此时\(y[i]\)表示的是\(s\)的第\(i\)个后缀按照前\(k\)个字母排序的离散化数值,所以\(s\)的第\(i\)个后缀的第\(k+1\) ~ \(2k\)个字母就是\(s\)的第\(i+k\)个后缀的前\(k\)个字母)

x[sa[1]] = 1, num = 1;
for (int i = 2; i <= n; i ++ )
if(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k]) 
x[sa[i]] = num;
else x[sa[i]]=++num;

注意y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k]是不会越界的,因为

image

最后,如果离散化值域达到了\(n\),则说明排序完成,否则的话令\(m\)为离散化值域重复上述过程

if (num == n) break;
m = num;

上述倍增法的时间复杂度为\(O(n\log n)\)

求\(height\)见OI-wiki

补充一个\(\text{lcp}\)的性质:设\(i<j\),则\(\text{lcp}(i,j)=\overset{j}{\underset{k=i}{\min}}(\text{lcp}(i,k),\text{lcp}(k,j))\),其中\(k\)是任取的(证明见视频00:37:20);由这个性质不难得出,如果想求\(\text{lcp}(i,j)\)的话,求一下\(\text{lcp}(i,i+1),\text{lcp}(i+1,i+2),...,\text{lcp}(j-1,j)\)并取最小值就好了

标签:后缀,字母,数组,sa,排序,2k
From: https://www.cnblogs.com/dingxingdi/p/18368976

相关文章

  • 两线程读写数组
    #include<stdio.h>#include<stdlib.h>#include<pthread.h>#include<unistd.h>#defineARRAY_SIZE10intshared_array[ARRAY_SIZE];pthread_mutex_tmutex;void*write_data(void*arg){intthread_id=*(int*)arg;......
  • 【数据结构与算法第一章】编程基础:变量与数据类型、指针、结构体、数组与链表、程序结
    目录【数据结构与算法第一章】编程基础1.1变量与数据类型1.2指针1.3结构体1.4数组和链表1.5程序结构1.6函数中参数的传递1.7C语言中运算符的含义【数据结构与算法第一章】编程基础1.1变量与数据类型变量:    ①在C语言中,所有变量必须先声明后使用......
  • Java数组02:数组内存分析、三种初始化方式及特点
    本节内容视频链接:Java数组03:三种初始化及内存分析_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV12J41137hu?p=53&vd_source=b5775c3a4ea16a5306db9c7c1c1486b51.数组内存分析堆:存放new的对象和数组;可以被所有线程共享,不会存放别的对象引用;栈:存放基本变量类型,会包含......
  • Java数组03:数组边界、数组的使用
    本节内容视频链接:https://www.bilibili.com/video/BV12J41137hu?p=55&vd_source=b5775c3a4ea16a5306db9c7c1c1486b5https://www.bilibili.com/video/BV12J41137hu?p=55&vd_source=b5775c3a4ea16a5306db9c7c1c1486b51.数组边界数组下标的合法区间[0,Length-1],如果越界就会报......
  • 用for循环输出数组与初识增强for循环
    1.定义一个数组2.使用for循环设置编码3.输出带有编码的数组使用增强for循环输出数组1.依旧是定义数组2.设置一个新的变量x用于替代数组3.直接输出变量x即可......
  • C语言程序设计-[23] 数组应用(续)
    1、输入一行字符,统计其中有多少个单词。根据以上分析,代码与结果如下:#include"stdio.h"intmain(){charc,pre,str[81];inti,n=0;gets(str);pre='';for(i=0;c=str[i];i++){ if(c!=''&&pre=='') {......
  • 数组
    Arrays类java.util.Arrayspackagecom.yang.array;importjava.util.Arrays;publicclassarraysDemo06{publicstaticvoidmain(String[]args){int[]a={1,3,121,3,4};System.out.println(a);//[I@6bdf28bb//打印数组元素rrays.toS......
  • 通过指针引用数组
    数组元素的指针数组指针:数组中的第一个元素的地址,也就是数组的首地址。指针数组:用来存放数组元素地址的数组,称之为指针数组。//定义一个一般数组inta[]={1,4,9};//使用指针变量存储数组的第一个元素的首地址,也就是数组的首地址int*p=&a[0];//数组首地址//在C......
  • 03-Matlab数组与矩阵
    数组的建立和操作数组算术运算数组信息获取矩阵的建立矩阵的扩展矩阵的块操作矩阵中元素的删除赋值为一对方括号矩阵的转置加点不转置为共轭复数没点的转置为共轭复数矩阵的旋转矩阵的翻转矩阵尺寸的改变矩阵加减法矩阵乘法矩阵除法矩阵中元素......
  • C/C++语言基础--指针三大专题详解2(指针与数组关系,动态内存分配,代码均可)
    本专栏目的更新C/C++的基础语法,包括C++的一些新特性前言指针是C/C++的灵魂,和内存地址相关联,运行的时候速度快,但是同时也有很多细节和规范要注意的,毕竟内存泄漏是很恐怖的指针打算分三篇文章进行讲解,本专题是二,介绍了指针和数组的关系、动态内存如何分配和释放等问题专题......