数据结构 —— 字符串:后缀数组

由于被虐得不要不要的,所以用此文纪念一下我(秃头)爆肝弄得似懂非懂的后缀数组——一个神奇的东西。

1.需求是什么?(应用)

我们在了解一个东西之前,先问,我们为什么要这个东西?它有什么用吗?所以我们先来讲讲它到底是怎么来的,为什么需要它?先建立了目标,我们就容易引出它的概念了。

  1. 首先,我们先思考一下一个最本质的问题:将从i(1<=i<=n)开始到字符串结尾的那部分字符串排序。
  2. //待更

2.后缀数组是啥?(定义)

(1)在了解后缀数组之前,我们先来看看两个概念:子串后缀

  • 子串:字符串中任意个连续的字符组成的子序列称为该串的子串。比如:字符串S=“ABCABCAA”,那么“ABC”、“A”、“CAA”、“ABCABCAA”都是它的子串。
  • 后缀:从某一位置i开始到字符串结尾的特殊子串。比如:字符串S=“ABCABCAA”,假设我们以1为开头,那么i=3的后缀就是:“CABCAA”。我们记为suffix(s,3);

(2)有了这两个概念以后我们就可以来定义两个数组了,第一个是Rank数组,第二个就是我们的SA数组(后缀数组【Suffix Array】)。

  • SA[i]:将所有后缀按字典序排序后第 i 小的后缀在原数组中的开始编号。
  • Rank[i]:第 i 位开始的后缀在后缀按字典序排序后的排名。

举个栗子:字符串S=“ABCA”,那么它的后缀分别是:“ABCA”、“BCA”、“CA”、“A”。那么将 他们按字典序排好就是:“A”、“ABCA”、“BCA”、“CA”;对应的SA={4,1,2,3}、Rank={2,3,4,1};例:SA[1]=4:后缀排名第一的是“A”,开始位置是原数组的最后一位,也就是第 4 位。
Rank[2]=3:从第 2 位开始的后缀为:“BCA”,在排序中的排名为第 3 位。
SA[i]说的是:第 i 大的我原来在哪?也就是我的下标是多少?
Rank[i]说的是:下标为 i 的我排第几?也就是我的排名是多少?

如果弄不清楚的,多举例试试,明晰定义很重要!

(3)我们有了这两个数组之后,我们先来看看这两个的关系,如果读者细心的话,会发现这两个数组存在着类似反函数一样的性质。为了便于区分,我们SA数组的下标用 l (小L)表示,Rank数组的下标用 i 表示,那么我们能得到这两个关系:

  • Rank[SA[l]]=l
  • SA[Rank[i]]=i

怎么去理解呢?可以试着问自己问题:SA存的是第 l 大的后缀在原数组下的开始位置,那么Rank[SA[l]] 表示的就是 以 第 l 大的后缀在原数组中的开始位置 为开始位置的后缀的排名是多少?很明显,就是 l 。下一条同理有: 以 原数组中第 i 位开始的数组的排名 为排名的后缀在原数组中的开始位置是多少? 很明显,就是 i 。有了这一对关系之后,我们就可以着手来求构造其中一个数组了,然后用关系去构造出另一个数组。

3.怎么构造后缀数组?(原理)

1)首先,我们有一个字符串S。我们先来简化一下问题,一下子后缀太难了。S的单个字符的SA数组与Rank数组是不是很好求呢?这个就相当于只是把字符串S排序一下而已啦,是不是?OK,那么我们得到了长度为1的子串的SA数组与Rank数组(类似的定义,只不过不是后缀,而仅仅只是子串)。

2)我们将问题升级一下,长度为2的子串的SA数组与Rank数组要怎么求呢?为了充分利用我们已经知道的信息,我们先把长度为分为前半截以及后半截(各长度1)。因为字典序嘛,所以我们先比较前半部分,如果相同,再比较后半部分,(比如:“AA”与“AB”)。那么就相当于先按前半部分排序,再按后半部分排序。(想清楚过程哦!)这样,我们就得到了长度为2的子串的SA数组与Rank数组了。

3)因为字符串好长好长好长,我们不得不继续做下去,但是我们似乎有点思路了哦?!想想我们再求多长的子串的SA数组与Rank数组呢? 3?!嘿嘿,不是的! 是长度为4的子串的SA数组与Rank数组哦!为什么呢?虽然我们也有长度为1的信息,但是啊,这个不如我们长度为2的信息那么有用哦,毕竟我们二分要优于不对等分的。依旧分成前半截与后半截来求。这样我们就驾轻就熟了,先按前半部分排序,再按后半部分排序。这样我们就能得到长度为4的子串的SA数组与Rank数组了。

4)一般地,对于长度为N的字符串S,我们只需要分log2N次就好啦。假设我们已经知道了长度为 i 的子串的SA与Rank了,我们想知道长度为 2*i 的子串的SA与Rank,我们只需要分成前半截与后半截,分别排序就好啦!由于我们是后缀数组,所以每个后缀的长度必定唯一并且必定有N个后缀(因为开始位置 i 不同),所以排名不会重复。当我们最终的子串扩展了log2N次时(后面子串扩展超过N的部分【就是后面没东西可以补了!】默认补上’\0’,值为最小值0。),那么子串的SA数组与Rank数组就扩展成了后缀数组。

这个就是:倍增法求SA数组与Rank数组的大体思路了。(还不了解的话,对着下图手动模拟一下啦!)


(没错,又是这个大名鼎鼎的图!)

下面我们要讨论具体的啦!所谓的“排序”,排序什么,怎么排?怎么知道一个数组,利用关系得到另一个数组?

5)要弄清楚排序什么,我们先要看我们比较的是什么。首先对于同一长度的两个字符串,他们两个按字典序比较的话,分成相同的部分与不同的部分。比如:“ABCCCCAA”与“ABCDDDBB”两个,公共部分是:“ABC”,后面明显“C”要小于“D”,故前者更靠前。所以我们二分的时候,两个字符串先比较前半部分。就是比较前半部分的字典序的排名,那么我们什么量描述排名呢?就是我们的Rank数组啦(不清楚的回过头去再看看Rank数组的定义)。所以我们比较的是第一个字符串S1前半部分的Rank与第二个字符串S2前半部分的Rank。而如果相同,那么就比较后半部分。

6)如果我们弄成pair对,丢进去排序的话,我们复杂度是nlogn的,而字符串比较是logn的,那么就会变成O(nlog2n),似乎也足够了。但是如果我们采用一个更好的线性排序的话,我们就可以降成O(nlogn),是不是帅气很多?!(对于大数据点我们还是得认怂啊。)那么我们的排序方法呢,就是大名鼎鼎(很少使用 )的基数排序啦!

7)最后一个问题,我们怎么由一个数组来得到另一个呢?假设我们知道了Rank[i],那么我们的SA[Rank[i]]=i啦!

(补充)基数排序

如果你已经很熟悉基数排序了,那么就直接看实现的代码吧。

1)基数排序是一种类似桶排一样的排序方法,就比如我们现在要排几个3位数,A={1,258,323,43,76};你会发现如果直接桶排,就要浪费好多好多的内存,毕竟你要开324个桶。那么有没有什么更好的方法能既高效又不用浪费那么大的内存呢?肯定有的啦,毕竟标题摆在这里 ,那就是基数排序了。基数排序具体来说:就是先按照低位排好序,然后再按更高位的排序,比如整数排序:我们开0-9个队列(数组模拟也可),分别按照个位、十位、百位顺序排序(不足的默认补0)。我们来模拟一次。

先按个位排序:1 323 43 76 258;
再按十位排序:01 323 43 258 76;
最后百位排序:001 043 076 258 323;

个位、十位、百位可以抽象成优先度,按照第一、第二、第三优先度排序。每次都用上次的排好序结果来再按另外的顺序排序,这种排序方法是稳定且高效的。

2)由于我们是需要基数排序来求SA数组,所以我们就给出求SA数组的代码实现,对整数排序的原理也是一样的(有兴趣自行百度):

for (i = 1; i <= n; ++i) ++cnt[a[i]];
for (i = 1; i <= m; ++i) cnt[i] += cnt[i - 1];
for (i = n; i >= 1; --i) sa[cnt[a[i]]--] = i;

我们来分析一下这段代码:
1.变量:
a[i]:待排序数组
cnt[i]:计数数组
sa[i]:排名为 i 的元素在原数组里的下标
cnt[a[i]]:表示我前面有都是个比我小的数
n:数据量
m:值域(相当于0~9)

2.语句:
第一个循环:相当于在第a[i]大的数的桶里计数
第二个循环:算前缀和
第三个循环:记录下原数组的位置

【解释:我前面有几个元素,我就排第几嘛(比如我跑步第四名、就意味着我的前面有4个数,不管他们并列与否,这个就是求前缀和的意义。)。“- -” 是因为我自己已经记录了,相当于把我自己pop出去。】

(图解:)
在这里插入图片描述

4.如何实现?(代码)

//后缀数组
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>

using namespace std;

const int N = 1000010;

char s[N];
int n, sa[N], rk[N], oldrk[N << 1<
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值