由于被虐得不要不要的,所以用此文纪念一下我(秃头)爆肝弄得似懂非懂的后缀数组——一个神奇的东西。
我们在了解一个东西之前,先问,我们为什么要这个东西?它有什么用吗?所以我们先来讲讲它到底是怎么来的,为什么需要它?先建立了目标,我们就容易引出它的概念了。
(1)在了解后缀数组之前,我们先来看看两个概念:子串与后缀。
(2)有了这两个概念以后我们就可以来定义两个数组了,第一个是Rank数组,第二个就是我们的SA数组(后缀数组【Suffix Array】)。
举个栗子:字符串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 表示,那么我们能得到这两个关系:
怎么去理解呢?可以试着问自己问题:SA存的是第 l 大的后缀在原数组下的开始位置,那么Rank[SA[l]] 表示的就是 以 第 l 大的后缀在原数组中的开始位置 为开始位置的后缀的排名是多少?很明显,就是 l 。下一条同理有: 以 原数组中第 i 位开始的数组的排名 为排名的后缀在原数组中的开始位置是多少? 很明显,就是 i 。有了这一对关系之后,我们就可以着手来求构造其中一个数组了,然后用关系去构造出另一个数组。
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出去。】
//后缀数组
#include
#include
#include
#include
using namespace std;
const int N = 1000010;
char s[N];
int n, sa[N], rk[N], oldrk[N << 1], id[N], px[N], cnt[N];
// px[i] = rk[id[i]](用于排序的数组所以叫 px)
bool cmp(int x, int y, int w)
{
return oldrk[x] == oldrk[y] && oldrk[x + w] == oldrk[y + w];
}
int main() {
int i, m = 300, p, w;
scanf("%s", s + 1);
n = strlen(s + 1);
for (i = 1; i <= n; ++i) ++cnt[rk[i] = s[i]];
for (i = 1; i <= m; ++i) cnt[i] += cnt[i - 1];
for (i = n; i >= 1; --i) sa[cnt[rk[i]]--] = i;
for (w = 1; w < n; w <<= 1, m = p) // m=p 就是优化计数排序值域
{
for (p = 0, i = n; i > n - w; --i) id[++p] = i;
for (i = 1; i <= n; ++i)
if (sa[i] > w) id[++p] = sa[i] - w;
memset(cnt, 0, sizeof(cnt));
for (i = 1; i <= n; ++i) ++cnt[px[i] = rk[id[i]]];
for (i = 1; i <= m; ++i) cnt[i] += cnt[i - 1];
for (i = n; i >= 1; --i) sa[cnt[px[i]]--] = id[i];
memcpy(oldrk, rk, sizeof(rk));
for (p = 0, i = 1; i <= n; ++i)
rk[sa[i]] = cmp(sa[i], sa[i - 1], w) ? p : ++p;
}
for (i = 1; i <= n; ++i) printf("%d ", sa[i]);
return 0;
}
(没错,就是恬不知耻从oi-wiki上copy下来的!)
因为板子太好了,所以博主就自作主张的拉下来了。下面我主要是帮助大家理解这段代码,如果碰到思路上不懂的,就回头看看,或者去oi-wiki,或者其他地方找资料反复看(肝),博主毕竟只是个小白,欢迎大神评论指正!!!
1.变量:
s[N]:字符串
sa[N]:SA数组
rk[N]:Rank数组
oldrk[N]:上一次的Rank数组
px[N]:排序数组
id[N]:按第二部分排好序后的数组下标(可以看成小SA数组)
cnt[N]:相当于桶排序里面的桶,就是值域桶。
n:字符串长度
m:值域
w:倍增长度
p:用于缩小值域的中间变量
2.语句块:
for (i = 1; i <= n; ++i) ++cnt[rk[i] = s[i]];
for (i = 1; i <= m; ++i) cnt[i] += cnt[i - 1];
for (i = n; i >= 1; --i) sa[cnt[rk[i]]--] = i;
我们一开始,把单个变量的ascii码当做他们的rank,然后直接基数排序得到长度为1的rank数组和sa数组。
for (w = 1; w < n; w <<= 1, m = p) // m=p 就是优化计数排序值域
{
for (p = 0, i = n; i > n - w; --i) id[++p] = i;
for (i = 1; i <= n; ++i)
if (sa[i] > w) id[++p] = sa[i] - w;
memset(cnt, 0, sizeof(cnt));
for (i = 1; i <= n; ++i) ++cnt[px[i] = rk[id[i]]];
for (i = 1; i <= m; ++i) cnt[i] += cnt[i - 1];
for (i = n; i >= 1; --i) sa[cnt[px[i]]--] = id[i];
memcpy(oldrk, rk, sizeof(rk));
for (p = 0, i = 1; i <= n; ++i)
rk[sa[i]] = cmp(sa[i], sa[i - 1], w) ? p : ++p;
}
然后这里就是倍增了,把w扩张2倍,然后更新值域,就是最外层的for了。我们把倍增的代码内部看看。
for (p = 0, i = n; i > n - w; --i) id[++p] = i;
for (i = 1; i <= n; ++i)
if (sa[i] > w) id[++p] = sa[i] - w;
这里是对第二部分进行排序,得到id数组。第一个for,是把 n-w < i <= n 拿出来,为什么呢,因为这部分是没有第二部分的。具体的来讲就是:i + w >= n。所以后面都是补‘\0’也就是0的。所以我们可以直接先把他们放进id数组里面。第二个for是把上一次的第二部分排序的内容,按照已经排好的顺序放进id数组里。(sa[i]-w,就是上一次的值嘛,你上次扩了一倍,现在减掉。)【这部分是优化以后的部分,实在不懂也可以用原始的基数排序对第二部分排序代替。】
memset(cnt, 0, sizeof(cnt));
for (i = 1; i <= n; ++i) ++cnt[px[i] = rk[id[i]]];
for (i = 1; i <= m; ++i) cnt[i] += cnt[i - 1];
for (i = n; i >= 1; --i) sa[cnt[px[i]]--] = id[i];
这部分是**对第一部分进行排序,得到sa数组。**结合第二部分的顺序,我们将Rank[id[i]]转换成px[i],减少不连续内存的访问,优化。【oi-wiki写的,优化原因博主也有点小懵,但是确实这样操作不容易错和TLE,至少不用套三个[],嘻。】
bool cmp(int x, int y, int w)
{
return oldrk[x] == oldrk[y] && oldrk[x + w] == oldrk[y + w];
}
memcpy(oldrk, rk, sizeof(rk));
for (p = 0, i = 1; i <= n; ++i)
rk[sa[i]] = cmp(sa[i], sa[i - 1], w) ? p : ++p;
这部分是利用已知的sa数组,更新rank数组。我们着重看看cmp函数:如果我的前半部分和上一个的前半部分一样;我们就看后半部分,如果还是一样,我们就一样的排序嘛。(只考两科,一科语文,一科英语。两个人都是300分,各自都是150。不就并列第一了嘛。)由于后缀开始位置各不相同,所以排到最后肯定排名不同,那么当排名不再更新的时候,我们就得到了我们的sa数组和rank数组啦!p是本次的最高分,用于缩小每次基数排序的值域桶。
一般对于一些后缀数组的题目,它并不是简单地要求对后缀进行排序,而是要用这个排序去O(n)的获得一个数组,Height数组。
1)首先,我们先了解一个概念:最长公共前缀(LCP【The longest common prefix】)。
- LCP:字符串S与T的最长公共前缀就是:对于xmax<=min(|S|,|T|)[xmax:最大的x];使得,对于任意1<=i<=x,S[i]==T[i];
举个栗子:S=“ABCAA”,T=“ABCDD”,LCP(S,T)=“ABC”。
2)了解了LCP之后,我们就可以定义Height数组了。
- Height[i]:LCP(SA[i],SA[i-1]),即第 i 名的后缀与它前一名的后缀的最长公共前缀的长度,其中,Height[1]=0;
举个栗子:S=“ABCA”,那么排序过后呢,就是:“A”、“ABCA”、“BC”、“C”。那么Heigh={0,1,0,0};
3)如何求Height数组?这里我们给出一个定理(证明在后面,有兴趣的可以去康康):
我们怎么理解呢,把Rank代入到Height的定义里面,得到的是 |LCP ( i , i - 1)| >= |LCP ( i-1,i-2 )|-1(这里的 i 指的是以 i 开头的后缀);其实就是相邻的两个后缀,后面不会短于上一个的长度减一。
4)代码实现:
for (i = 1, k = 0; i <= n; ++i)
{
if (k) --k;
while (s[i + k] == s[sa[rk[i] - 1] + k]) ++k;
ht[rk[i]] = k; // height太长了缩写为ht
}
(没错,又是直接拉的!!)
我们来分析一下吧:
k:LCP的长度。
if语句:我们至少是上一个的长度减一嘛,那我直接减一就好啦。
while语句:前面k个保证相等了(此时k已经减过了),我只要去对比后面。那么等号前面可以看成是以第 i 位开头的后缀的第 k 个字符,与以第 i 个字符开头的后缀的按字典序排序好的数组里面的上一位的第 k 个字符,如果相同,那么长度加一。
最后一句就是记录值了。
(图解):
(画得不是很好,如果错误还请指正!!)
5)复杂度分析:
我们的k是不会超过n的,那么最多减n次,最多加2n次,所以复杂度就是O(n)的啦。
6):证明:
//待补