首页 > 其他分享 >后缀数组学习笔记

后缀数组学习笔记

时间:2025-01-02 21:53:15浏览次数:1  
标签:LCP 后缀 笔记 数组 sa 排序 operatorname rk

\(\text{后缀数组学习笔记}\)

一、定义

对于下标从 \(1\) 开始,长度为 \(n\) 的字符串 \(s\),我们定义后缀 \(i\) 表示字符串 \(s[i, n]\)。

对于后缀数组,我们定义 \(sa(i)\) 表示所有后缀按字典序排序后第 \(i\) 小的后缀的编号。

例如对于字符串 aabaaab,它有 \(7\) 个后缀,下边我们列出排序后的结果:

  • aaab
  • aab
  • aabaaab
  • ab
  • abaaab
  • b
  • baaab

那么容易得到 \(sa(1)=4,sa(2)=5,\cdots\)。和常见求排名的数据结构一样,同样需要定义一个 \(rk(i)\) 表示每个后缀的排名。例如 \(rk(4)=1,rk(5)=2,\cdots\)。

容易发现的是 sa[rk[i]] = rk[sa[i]] = i

二、后缀数组的求解

1. 暴力

直接预处理出 \(s\) 的 \(n\) 个后缀再排序。排序的复杂度是 \(O(n\log n)\),字符串的比较是 \(O(n)\),于是总的复杂度是 \(O(n^2\log n)\)。

2. 倍增

(1) 倍增求解后缀数组

考虑我们比较两个字符串大小的过程。考察对于两个长度为 \(2n\) 的字符串 \(s,t\),\(s>t\) 时的条件:

  • \(s[1,n]>t[1,n]\)
  • 或是 \(s[1,n]=t[1,n]\) 且 \(s[n+1,2n]>t[n+1,2n]\)。

那么我们将长度为 \(n\) 的字符串拆成了两个长度为 \(\dfrac{n}{2}\) 的字符串来比较。这样将大问题减半拆解的做法显然可以倍增解决。

于是让我们描述这个做法的过程:

  1. 首先对每个字符排序,得到 \(sa_1,rk_1\)。
  2. 套用上面的过程,用 \(rk_1(i),rk_1(i+1)\) 作为第一、二关键字排序,得到长度为 \(2\) 的子串的排名。
  3. 每次都这样做,用长度为 \(\dfrac{w}{2}\) 的子串的排名得到长度为 \(w\) 的子串的排名。
  4. 考虑这样做的疏漏:对于 \(rk_{\frac{w}{2}}(i+\frac{w}{2})\),这个下标存在 \(>n\) 的情形,此时视为 \(0\) 即可。对于长度 \(<w\) 的后缀,并不存在子串 \(s[i,i+w-1]\),此时将子串视为 \(s[i,n]\)。
  5. 当 \(w\ge n\) 时,此时的 \(sa_w,rk_w\) 就是所求。

对于复杂度的分析,倍增是 \(O(\log n)\) 的,排序是 \(O(n\log n)\) 的,于是复杂度是 \(O(n\log^2 n)\)。

(2) 基数排序优化

考虑复杂度的可优化部分:倍增是难以优化的,但是对于排序,我们可以浅浅利用一番其性质。注意到每次排序的是一个二元组,于是考虑进行基数排序优化。那么让我们插播一下基数排序的内容。

基数排序是建立在桶排序的基础之上的。本质上是一个根据优先级逐层确定顺序的过程。对于一些正整数的排序过程是这样的:

  1. 先按照个位的不同排序,放入桶中;
  2. 再按照十位的不同排序,但要注意放入桶中的顺序是个位排序后的顺序;
  3. 依次排完所有十进制位。

对于这样做的正确性,事实上体现着基数排序的核心就是多关键字的排序。对于复杂度分析,令 \(V\) 表示值域,\(m\) 表示关键字的个数,那么桶排序是复杂度是 \(O(n+V)\),那么总的复杂度就是 \(O(mn+mV)\)。

那么对于后缀数组的排序,\(m=2,a\le n\),那么总的复杂度就是 \(O(n)\) 的。这样一来,总的复杂度就是 \(O(n\log n)\)。

(3) 一些常数优化

  1. 第二关键字无需桶排序。原因是对于 \(i\in[n-w+1,n]\),它们的值一定是 \(0\),一定最小,直接扔到数组最前面即可。对于剩下的后缀,有 \(sa(i)>w\),于是放入和它拼接而成的 \(sa(i)-w\) 即可。
  2. 每次基数排序的值域设为 \(rk\) 的值域即可。
  3. 若 \(rk\) 的值域已经为 \(n\),显然可以直接跳出。

代码:

void SA(int M) {
	int m = M, p = 0;
	for (int i = 1; i <= n; i++) cnt[rk[i] = s[i]]++;
	for (int i = 1; i <= m; i++) cnt[i] += cnt[i - 1];
	for (int i = n; i >= 1; i--) sa[cnt[rk[i]]--] = i;
	for (int w = 1;; w <<= 1, m = p) {
		int cur = 0;
		for (int i = n - w + 1; i <= n; i++) id[++cur] = i;
		for (int i = 1; i <= n; i++)
			if (sa[i] > w) id[++cur] = sa[i] - w;
		for (int i = 1; i <= m; i++) cnt[i] = 0;
		for (int i = 1; i <= n; i++) cnt[rk[i]]++;
		for (int i = 1; i <= m; i++) cnt[i] += cnt[i - 1];
		for (int i = n; i >= 1; i--) sa[cnt[rk[id[i]]]--] = id[i];
		swap(lk, rk);
		p = 0;
		for (int i = 1; i <= n; i++) {
			if (lk[sa[i]] == lk[sa[i - 1]] && lk[sa[i] + w] == lk[sa[i - 1] + w]) rk[sa[i]] = p;
			else rk[sa[i]] = ++p;
		}
		if (p == n) break;
	}
}

其中 M 是初始的值域范围,由题意得到。

然而只会求后缀数组并没有什么用处,我们还需要知道一些奇妙的性质。

三、\(\operatorname{LCP}\) 的一些性质

性质 \(1\)

\[\operatorname{LCP}(sa(i),sa(k))=\min(\operatorname{LCP}sa(i),sa(j)),\operatorname{LCP}(sa(j),sa(k)))(i<j<k) \]

我们记 \(p=\min(\operatorname{LCP}(sa(i),sa(j)),\operatorname{LCP}(sa(j),sa(k)))\)。那么 \(sa(i)\) 和 \(sa(k)\) 的前 \(p\) 位是相等的。于是 \(\operatorname{LCP}(sa(i),sa(k))\ge p\)。

对于 \(sa(i)\) 和 \(sa(k)\) 的第 \(p+1\) 位,我们记 \(sa(i),sa(j),sa(k)\) 的第 \(p+1\) 位为 \(w(i),w(j),w(k)\),分类讨论:

  • \(\operatorname{LCP}(sa(i),sa(j))=\operatorname{LCP}(sa(j),sa(k))=p\),此时显然第 \(p+1\) 位的 \(sa(i),sa(j)\) 不同,且 \(w(i)<w(j)<w(k)\)。
  • \(\operatorname{LCP}(sa(i),sa(j))>\operatorname{LCP}(sa(j),sa(k))=p\),此时 \(w(i)=w(j)<w(k)\)。
  • \(\operatorname{LCP}(sa(j),sa(k))>\operatorname{LCP}(sa(i),sa(j))=p\),此时 \(w(i)<w(j)=w(k)\)。

也就是无论如何,\(w(i)<w(k)\),于是 \(\operatorname{LCP}(sa(i),sa(j))\le p\),于是 \(\operatorname{LCP}(sa(i),sa(j))=p\)。

性质 \(2\)

\[\operatorname{LCP}(sa(i),sa(j))=\min_{k=i+1}^{j}\operatorname{LCP}(sa(k-1),sa(k)) \]

这样一条性质由性质 \(1\) 是容易通过数学归纳法得到的。

性质 \(3\)

\[\operatorname{LCP}(sa(j),sa(k))\ge\operatorname{LCP}(sa(i),sa(k))(i\le j\le k) \]

这个性质可以通过 \(\min\) 的不升性结合性质 \(2\) 得到。

四、\(\operatorname{Height}\) 数组

1. 定义

我们定义 \(\operatorname{LCP}(i,j)\) 表示 \(s\) 中两个后缀 \(i,j\) 的最长公共前缀。

定义 \(\operatorname{Height}(i)=\operatorname{LCP}(sa(i),sa(i-1))\),规定 \(\operatorname{Height}(1)=0\)。以下为了方便,用 \(h\) 代指 \(\operatorname{Height}\) 数组。

2. 求法

引理:\(h(rk(i))\ge h(rk(i-1))-1\)。

考虑证明。若 \(h(rk(i-1))\le 1\),原式显然成立,否则有 \(h(rk(i-1))>1\),那么考虑后缀 \(i,j\),其中有 \(rk(j)+1=rk(i)\),那么当两个后缀的首字母相同时,显然删掉这个首字母两个新的后缀 \(j+1,i+1\) 的排名的先后顺序是不变的,有 \(rk(j+1)<rk(i+1)\),那么有 \(rk(j+1)\le rk(i+1)-1\)。结合性质 \(3\) 可推出

\[h(rk(i+1))=\operatorname{LCP}(sa(rk(i+1)),sa(rk(i+1)-1))\ge \operatorname{LCP}(sa(rk(i+1)),sa(rk(j+1)))=\operatorname{LCP}(i+1,j+1) \]

根据定义 \(h(rk(i))=\operatorname{LCP}(sa(rk(i)),sa(rk(i)-1))=\operatorname{LCP}(i,j)=\operatorname{LCP}(i+1,j+1)+1\),那么移项后得到 \(h(rk(i+1))\ge h(rk(i))-1\),证毕。

标签:LCP,后缀,笔记,数组,sa,排序,operatorname,rk
From: https://www.cnblogs.com/Rock-N-Roll/p/18648804

相关文章

  • 2025-1-1 / 2025-1-2 做题笔记
    2025-1-1/2025-1-2做题笔记持续更新中……目录2025-1-1/2025-1-2做题笔记CF1534H-LostNodesCF1510B-ButtonLockCF1336E1-ChioriandDollPicking(easyversion)UOJR28B-环环相扣ATNOMURA2020F-SortingGameATJOISC2017E-壊れた機器(BrokenDevice)SYS......
  • [数据结构学习笔记2] 大O法表示程序的时间复杂度
    程序运行都是需要时间的,我们用大O法来表示,程序在最坏情况下,运行时间和输入规模的关系。一般有这么几种大O时间:快:闪电:O(1)-常量复杂度-和输入无关;比如通过数组下标访问元素;简单数学运算,如看末尾数字是否是基数;火箭:O(logn)-对数复杂度-时间增长随数字规模增长缓慢;这种......
  • [数据结构学习笔记1] 为什么需要有数据结构
    程序本质上就是用来读取数据,然后操作数据,最后生成数据的。如果数据能被有效,或者有结构的展现,那将极大方便程序操作。举例:我们家里有很多工具,剪刀,锤子,斧头,扳手,放大镜,六角扳手,螺丝刀,尺子,卷尺,螺丝,便利贴等等。我们可以怎样收纳这些工具,使得我们后续可以方便的使用呢?第一种,我们家有......
  • 以太坊 solidity 笔记
    基础知识gasgas是衡量执行某些操作所需的计算量的单位,用来计算为了执行操作而需要支付给网络的费用数额。通俗理解,Gas是给矿工的佣金,并以ETH支付,无论是交易、执行智能合约并启动DApps,还是支付数据存储费用,都需要用到Gas。Gas的目的是限制执行交易所需的工作量,同时为执行......
  • Java学习笔记06-多态polymorphism
    一、多态1、含义:多态是在继承/实现情况下的一种现象,表现为:对象多态、行为多态多态的具体代码体现:packageorg.example.polymorphism1;publicclassAnimal{publicStringname="动物";publicvoidrun(){System.out.println("动物跑");}......
  • Java学习笔记05-继承extends-03
    一、构造器用this(...)调用兄弟构造器作用:在构造器中调用本类的其他构造器注意事项:super(...)和this(...)必须写在构造器的第一行,且二者不能同时出现。代码演示:1、首先定义一个学生类,写好get和set方法,构造器和重写toString()方法packageorg.example.extends6constructor;......
  • 线性基学习笔记
    线性基很好理解,可以理解成\(n\)维的向量。我们先考虑\(n=2\),这是我们最熟悉的,可以在平面直角坐标系上表示出来。众所周知,在一个平面内,两个不共线的向量\(e_1\)和\(e_2\),可作为基底,即所有可在原坐标系上表示的向量\(x\)均可被\(e_1\)和\(e_2\)表示为\(\lambdae_1+......
  • 【学习笔记】图的连通性相关
    1.无向图的连通性见【学习笔记】无向图的连通性。2.圆方树2.1定义&性质圆方树用来解决需要无向图按点双缩点的问题。这里的点双指的是无割点极大连通子图。由割点的性质可得,不同的点双之间,实际上是通过割点来连接的。那么怎么“缩点”?事实上,对于点双来讲,应该叫“缩边......
  • 学习笔记:C#高级进阶语法——反射
    二、反射(Reflection)1、反射:就是一个微软提供的帮助类库,可以解析并使用元数据。2、exe/dll----ILSpy工具打开会包含matedata(元数据),描述了当前程序集中有哪些元素成员3、反射动态加载dll文件,调用方法{//1、引用,2、new,3、调方法IDBHelperhelper=newSqlServerHel......
  • 寻找两个正序数组的中位数(二分查找)
    给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。算法的时间复杂度应该为 O(log(m+n)) 。 示例1:输入:nums1=[1,3],nums2=[2]输出:2.00000解释:合并数组=[1,2,3],中位数2示例2:输入:nums1=[1......