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

后缀数组小计

时间:2024-01-28 22:15:17浏览次数:26  
标签:log space 后缀 小计 数组 sa 排序 rk

0 前言

被迫上班了属于是

前置知识

  • 倍增
  • 基数排序

基数排序

这个很好理解
举个例子 : 对 \(n\) 对二元组 \((x,y)\) 排序 优先比较关键字 \(x\) 相同再比较关键字 \(y\)
第一步:以 \(y\) 为基准 从小到大把所有二元组排序 得到 \(y\) 递增的序列
第二部:以 \(x\) 为基准 从小到大把所有二元组排序 得到 \(x\) 递增的序列
因为保证原数列顺序不变 在 \(x\) 递增的同时 \(y\) 也是递增的 排序完毕
令值域为 \(w\) 时间复杂度为 \(O(w)\)
举个例子:
原 : \((3,4)\space (3,1)\space (1,5)\space (2,4)\)
第一次排序 : \((3,1)\space (3,4)\space (2,4)\space (1,5)\)
第二次排序 : \((1,5)\space (2,4)\space (3,1)\space (3,4)\)
听起来很简单 但是代码可不简单

void Qsort() {
	rep(i, 1, m) a[i] = 0;
	rep(i, 1, n) ++ a[rk[i]];
	rep(i, 1, m) a[i] += a[i - 1];
	drep(i, 1, n) sa[a[rk[tp[i]]] --] = tp[i];
}

来分析一下目前最同用的伪代码

1 基本概念

我们规定如下的数组:

  • \(rk[i]\) 表示开头下标为 \(i\) 的后缀的排名
  • \(sa[i]\) 即常说的后缀数组 表示排名为 \(i\) 的后缀开头下标

显然 有性质 \(sa[rk[i]]=rk[sa[i]]=i\)
理解一下:
对于 \(sa[rk[i]]=i\) 表示取出第 \(i\) 个后缀的排名 再引用该排名的位置即 \(i\)
对于 \(rk[sa[i]]=i\) 表示取出排名为 \(i\) 的后缀的位置 再求出该位置的排名即 \(i\)

(OI wiki 的图)
image

2 后缀数组求法

不难发现 只需要求出 \(rk\) 数组 \(sa\) 数组自然就能求出

1.\(O(n^2\log n)\)

这个肯定自己想都能想出来
先 \(O(n)\) 枚举所有后缀 然后使用快排
因为字符串比较时间是 \(O(n)\) 的 因此总的时间复杂度是\(O(n\times n\log n)\) 即 \(O(n^2\log n)\)

2.\(O(n\log^2 n)\)

我们发现时间瓶颈 \(n\) 卡在了 \(O(n)\) 的比较上
我们想想怎么优化排序 于是倍增法就出来了
主要思想就是一部分 一部分粘合

(OI wiki)
image
这一部分很好理解
以 最长的后缀 (下标从 \(1\) 开始为例)

首先 得到单个字母的 \(rank\) 值 (第一步)
然后 求出子串 \([1\to 2]\) 当前的 \(rank\)
怎么求?很好理解 记录第一关键字 \(x=rk1\) 第二关键字 \(y=rk2\)
排序所有的 \((x,y)\) 二元组可以得到所有当前的后缀排名
这时 会发现最后一位已经无法向后拓展了 (即第二次排序时最后的第二关键字)
那么 这时 记这位为 \(0\)
思考一下 这样 就与字符串比较的规则相符了

然后 同理 第三次求出子串 \([1\to 4]\)
第四次求出子串 \([1\to 8]\)

这样 不难发现 当所有的数第二关键字为 \(0\) 时 此时便得到了所有后缀
此时 第一关键字便是 \(sa\) 数组
非常巧妙的方法 这样 将比较是时间复杂度缩短为常数级别


现在分析时间复杂度
不难发现 倍增的次数为 \(O(\log n)\)
每次使用快排 时间复杂度 \(O(n\log n)\)

总体时间复杂度 \(O(n\log^2 n)\)


别看理论简单 代码却很抽象

#include<bits/stdc++.h>
#define N 1000005
using namespace std;
char s[N]; 
int n;
int sa[N],rk[N*2],lrk[N*2];
int k;
bool cmp(int a,int b)
{
	return rk[a]^rk[b]?rk[a]<rk[b]:rk[a+k]<rk[b+k];//先以第一关键字排序 再以第二关键字排序 
}
int main()
{
	scanf("%s",s+1);
	n=strlen(s+1);
	for(int i=1;i<=n;i++) sa[i]=i,rk[i]=s[i];//初始化 将第一次排名处理好  存入未排序的sa数组 
	for(k=1;k<n;k<<=1)
	{
		sort(sa+1,sa+1+n,cmp);//算出当前子串数组的sa 下面的步骤就是用sa推rk 
		memcpy(lrk,rk,sizeof rk);//复制一份rk数组 
		int p=0;
		for(int i=1;i<=n;i++)
			if(lrk[sa[i]]==lrk[sa[i-1]]&&lrk[sa[i]+k]==lrk[sa[i-1]+k])
				rk[sa[i]]=p;//枚举排名 i 比较与上一个拼接是否相同 相同就不是增大的排名 
			else rk[sa[i]]=++p;
	}
	for(int i=1;i<=n;i++) printf("%d ",sa[i]);
	return 0;
}

注意几个细节 \(k\) 是全局变量 否则无法参加 cmp 函数的计算
理解难点在于用 \(sa\) 推 \(rk\)
以求 \(rk_2\) 为例

  • step \(1\) 通过 \(sa\) 数组取出在新 \(rk\) 中排名为 \(1\space 2\) 的位置
  • step \(2\) 比较在旧 \(rk\) 中排名为 \(1\space 2\) 时的拼接是否相同
  • step \(3\) 如果有效 在新 \(rk\) 中 排名为 \(1\space 2\) 位置的值

3.\(O(n\log n)\)

非常简单的优化
容易发现 瓶颈在于快排
发现有 \(x,y\le n\) 的性质
双关键字排序将快排换为基数排序即可

理论结束 建议两种代码都写一次

标签:log,space,后缀,小计,数组,sa,排序,rk
From: https://www.cnblogs.com/g1ove/p/17993230

相关文章

  • 无涯教程-Swift - 数组
    Swift4数组用于存储相同类型的值的有序列表。如果将创建的数组分配给变量,则它始终是可变的,这意味着您可以通过添加,删除或更改其元素来对其进行更改;但是,如果将数组分配给常量,则该数组是不可变的,并且其大小和内容无法更改。创建数组您可以使用以下初始化器语法创建某种类型的空......
  • 后缀数组
    后缀数组定义suf[i]i到最后的子串rank[i]suf[i]在所有后缀中的排名sa[i]排名为i的后缀的开始位置sa[i]与rank[i]为互逆操作,相反的排列height[i]suf[sa[i]]与suf[sa[i-1]]的最长公共前缀H[i]即Height[rank[i]]求SA的倍增算法用基数排序,排序次数为\(\lceil......
  • 差分数组
    构造差分数组int*constructDifferenceArray(int*nums,intlength){int*diff=(int*)malloc(length*sizeof(int));diff[0]=nums[0];for(inti=1;i<length;i++){diff[i]=nums[i]-nums[i-1];}returndiff;}通过这个diff差分数组是可以反推出原......
  • 数组前缀和
    一维数组中的前缀和https://leetcode.com/problems/range-sum-query-immutable/description/区域和检索#include<stdio.h>#include<stdlib.h>//定义NumArray结构体typedefstruct{int*preSum;//前缀和数组}NumArray;//创建NumArray对象,并计算前缀和数组......
  • Java中的数组
    数组1、数组概述数组是相同类型数据的有序集合数组描述的是相同类型的若干个数据,按照一定的先后次序排列组合而成其中,每一个数据称作一个数组元素,每个数组元素可以通过一个下标来访问它们2、数组声明创建首先必须声明数组变量,才能在程序中使用数组。下面是声明数......
  • 数组:讲解一维数组和多维数组的声明、初始化和访问。
    一维数组是最简单的数组形式,它可以存储一系列相同类型的元素。多维数组则是由多个一维数组组成的数据结构,可以用于存储多维数据。下面我将分别讲解一维数组和多维数组的声明、初始化和访问。一维数组一维数组的声明和初始化可以分为两步进行。声明一维数组在C语言中,可以使用以下语......
  • 80. 删除有序数组中的重复项 II(中)
    目录题目题解:双指针题目给你一个有序数组nums,请你原地删除重复出现的元素,使得出现次数超过两次的元素只出现两次,返回删除后数组的新长度。不要使用额外的数组空间,你必须在原地修改输入数组并在使用O(1)额外空间的条件下完成。说明:为什么返回数值是整数,但输......
  • 动态区间求和——树状数组的基本应用
    目录前言问题引入思路一览具体分析前言准确来说,不应该叫做动态区间求和问题,只能说树状数组经常用于求和操作而已,但是正常的动态条件查询树状数组也是可以做的,具体的下面再说吧问题引入给出一个数组a[],要求做两种操作,一种是修改数组中的一个数,一种是实现查询区间[lt,rt](lt<=......
  • C#数组对象池ArrayPool<T>底层
    深度解析C#数组对象池ArrayPool<T>底层原理 提到池化技术,很多同学可能都不会感到陌生,因为无论是在我们的项目中,还是在学习的过程的过程,都会接触到池化技术。池化技术旨在提高资源的重复使用和系统性能,在.NET中包含以下几种常用的池化技术。(1)、连接池(Connecti......
  • 【板子】后缀自动机(SAM)
    //lg3804//Copyrightyeyou26//注意:这道题不是纯纯的后缀自动机,main函数有一点额外处理#include<bits/stdc++.h>usingnamespacestd;#definelllonglong#defineN(int(2e6+6))intfa[N];intch[N][28];intlen[N];intcnt[N];llans;intidx=1;intnp=1;str......