后缀数组
前排提醒:坑未填完。【如果你看到这句话,请提醒博主填坑】
0.参考资料
国家集训队2009论文——罗穗骞
Hihocoder关于后缀数组的讲解
比较详细的一篇博客
【以及刘汝佳的小蓝书 和 我们学校内部的课件】
1.相关定义
【这里我们总假设数组/字符串的起始下标为 0,终止下标为 len-1】
定义 suffix(i) 为从 i 开始到串尾的后缀,称为“后缀i”。
将字符串的所有后缀按字典序排序,可以得到这个字符串的后缀数组 sa(suffix array)。其中sa[i] 表示第 i 小的后缀为 suffix(sa[i])。
名次数组 rank 是 sa 的逆运算,即rank[sa[i]] = i,表示 suffix(i) 在所有后缀中的排名。因为长度不同的字符串不可能有相同的字典序,所以任意后缀的名次不并列。
【通俗地讲:sa[i] 表示排第 i 位的是谁,rank[i] 表示 i 排第几】【请一定要记住定义!!!】
假定某个字符串 S = “aabaaaab”。在它的末尾加入一个字符串中没有出现的字符比如”#”(等会儿我们解释为什么要加入尾字符)(原本应该是“$”来着……但是markdown会识别出来奇怪的东西)下面是它的sa和rank。
(UPD:原来还有转义字符这玩意儿……是博主low了)。
(假设 # 的优先级比字母小……哦不好像字典序的确 # 的优先级比字母小所以用不着假设233)
2.倍增算法
看到这里,我们大概可以瞎BB一个算法出来了:直接用sort进行排序。一共 n 个后缀,因此总复杂度 O(nlog2n) O ( n l o g 2 n ) 。因此,本算法被在合理时间复杂度内完美解决……?
注意到字符串之间的比较与字符串长成正比,所以算法复杂度将会是 O(n2log2n) O ( n 2 l o g 2 n ) 。平凡的 n 个字符串进行排序的复杂度是 O(n2log2n) O ( n 2 l o g 2 n ) 的(UPD:其实用字符串哈希可以得到一个O(nlog^2n)字符串排序算法来着,但是毕竟哈希的正确性并不稳定……),但是因为后缀之间是有联系的,所以我们可以设计出一个更优秀的算法:
该算法基于这样一个事实:两个字符串的比较,如果前半部分相同则比较结果决定于后半部分的比较结果,否则比较结果决定于前半部分的比较结果。
对于字符串
S
S
,定义k-前缀为:
Sk=S(k>lenS)
S
k
=
S
(
k
>
l
e
n
S
)
然后,我们令
sak[i]
s
a
k
[
i
]
为k-前缀意义下的后缀数组,
rankk[i]
r
a
n
k
k
[
i
]
为k-前缀意义下的名次数组。
一、容易求出
rank1
r
a
n
k
1
与
sa1
s
a
1
的值,只需要sort排序一次即可。
二、如果我们求出了
sak
s
a
k
,我们可以很快的求出
rankk
r
a
n
k
k
:
rankk[i]=rankk[i−1](suffix(sa[i])k=suffix(sa[i−1])k)
r
a
n
k
k
[
i
]
=
r
a
n
k
k
[
i
−
1
]
(
s
u
f
f
i
x
(
s
a
[
i
]
)
k
=
s
u
f
f
i
x
(
s
a
[
i
−
1
]
)
k
)
rankk[i]=rankk[i−1]+1(suffix(sa[i])k≠suffix(sa[i−1])k)
r
a
n
k
k
[
i
]
=
r
a
n
k
k
[
i
−
1
]
+
1
(
s
u
f
f
i
x
(
s
a
[
i
]
)
k
≠
s
u
f
f
i
x
(
s
a
[
i
−
1
]
)
k
)
三、然后,假如说我们求出了
rankk
r
a
n
k
k
,则定义二元组
(rankk[i],rankk[i+k])
(
r
a
n
k
k
[
i
]
,
r
a
n
k
k
[
i
+
k
]
)
。以“
rankk[i]相等,则比较rankk[i+k],否则比较rankk[i]
r
a
n
k
k
[
i
]
相
等
,
则
比
较
r
a
n
k
k
[
i
+
k
]
,
否
则
比
较
r
a
n
k
k
[
i
]
”的排序规则进行sort排序,即可求出
sa2k
s
a
2
k
。
也就是说算法流程是这样的:
什么时候停止呢?当不存在并列的rank的时候,所有后缀的大小关系就会被定义出来,这时候就可以停止算法了。
(图片来源百度百科)
实际上第三次排序完就可以停止算法了。
每一次排序需要
nlog2n
n
l
o
g
2
n
次比较,每一次比较需要
O(1)
O
(
1
)
的时间复杂度,因此总时间复杂度为
O(nlog22n)
O
(
n
l
o
g
2
2
n
)
【快排我就不给参考代码了大家直接理解桶排序的代码好嘛】【怠惰的博主】
3.基数排序(桶排序)
O(nlog22n) O ( n l o g 2 2 n ) 的时间复杂度尽管很好,但还是不够优秀。
咱们先从后缀数组前走开,探究这样一个问题:
假设有 n 个数进行排序,则排序时间复杂度下限为
O(nlog2n)
O
(
n
l
o
g
2
n
)
。
但假设保证这n个数都是两位数,我们可以做到
O(n)
O
(
n
)
的算法。怎么做?看一下下面的例子。
虽然实际运用中数据不可能总是两位数,但是假如我们把两位数看作一个二元组,十位看作第一关键字,个位看作第二关键字,并保证二元组的元素在能够存储的范围内,我们就可以实现对二元组的排序了。这种排序被形象地称为“桶排序”(基数排序)
我们先对字符串的元素进行离散化,元素的值域就变成了[0…n-1]。且离散化后我们发现 rank1[i]=S[i] r a n k 1 [ i ] = S [ i ] ,一举双得。然后每一次迭代中,不难发现 rank r a n k 和 sa s a 都不会超过 n−1 n − 1 (想想它们所代表的含义)。
于是得出结论:我们可以用基数排序代替每一轮循环中的sort。
一共进行
log2n
l
o
g
2
n
次排序,每次排序遍历n个元素,n个桶,总时间复杂度
O(nlog2n)
O
(
n
l
o
g
2
n
)
【后缀数组还存在 O(n) O ( n ) 的构造方法 dc3 d c 3 ,但代码相对复杂。因为对于大多数题来说 O(nlog2n) O ( n l o g 2 n ) 的复杂度已经足够了,所以此处不再讨论。有兴趣的读者可以去游览一下集训队大佬的论文orz(其实是我自己没学懂233)】
参考实现如下(作者根据自己的理解所写的代码,与网上大部分代码不大相同,但是作者表示他觉得效率差不多)(因为网上的代码作者也看不懂233):
int arr[MAXN + 5], rnk[MAXN + 5], nrnk[MAXN + 5];
int bin[MAXN + 5], nxt[MAXN + 5], sa[MAXN + 5];
void InsertBIN(int x, int k) {
nxt[x] = bin[k];
bin[k] = x;
}
void GetSAFromBIN(int n) {
int cnt = 0;
for(int i=0;i<n;i++) {
while( bin[i] != -1 ) {
sa[cnt++] = bin[i];
bin[i] = nxt[bin[i]];
}
}
}
void BuildSA(int n) {
for(int i=0;i<n;i++)
rnk[i] = arr[i];
for(int i=0;i<n;i++)
bin[i] = -1;
for(int i=0;i<n;i++)
InsertBIN(i, arr[i]);
GetSAFromBIN(n);
for(int k=1;rnk[sa[n-1]]!=n-1;k<<=1) {
for(int i=n-1;i>=0;i--)
if( sa[i] >= k )
InsertBIN(sa[i]-k, rnk[sa[i]-k]);
for(int i=n-k;i<n;i++)
InsertBIN(i, rnk[i]);
GetSAFromBIN(n);
for(int i=0;i<n;i++)
nrnk[i] = rnk[i];
rnk[sa[0]] = 0;
for(int i=1;i<n;i++) {
int del = (nrnk[sa[i]] != nrnk[sa[i-1]]) || (nrnk[sa[i]+k] != nrnk[sa[i-1]+k]);//注1
rnk[sa[i]] = rnk[sa[i-1]] + del;
}
}
}
【注1:这里sa[i]+k会访问到非法内存吗?不会。假如说
nrnk[sa[i]]==nrnk[sa[i−1]]
n
r
n
k
[
s
a
[
i
]
]
==
n
r
n
k
[
s
a
[
i
−
1
]
]
,则两个后缀的前半段一定不包含尾字符“#”,所以不会访问出界。否则因为C++短路运算符的特性,后面的语句不会执行】
【注2:实际上尾字符不一定要是“#”,让字符串长度++,字符串末尾就会多出现‘\0’这个字符,充当了尾字符的作用】
4.高度(height)数组
光有后缀数组并没有用的,我们还要借助它解决这样一个问题:LCP(Longest Common Prefix,最长公共前缀)。LCP的应用范围就非常广泛了,详情可以见下面的例题。
怎么解决呢?我们令
LCP(i,j)=lcp(suffix(i),suffix(j))
L
C
P
(
i
,
j
)
=
l
c
p
(
s
u
f
f
i
x
(
i
)
,
s
u
f
f
i
x
(
j
)
)
,并令
height[i]=LCP(sa[i],sa[i−1])
h
e
i
g
h
t
[
i
]
=
L
C
P
(
s
a
[
i
]
,
s
a
[
i
−
1
]
)
。对于满足
rank[i]<rank[j]
r
a
n
k
[
i
]
<
r
a
n
k
[
j
]
,有这样一个结论:
LCP(i,j)
L
C
P
(
i
,
j
)
=min(height[rank[i]+1],height[rank[i]+2],...height[rank[j]])
=
m
i
n
(
h
e
i
g
h
t
[
r
a
n
k
[
i
]
+
1
]
,
h
e
i
g
h
t
[
r
a
n
k
[
i
]
+
2
]
,
.
.
.
h
e
i
g
h
t
[
r
a
n
k
[
j
]
]
)
=RMQ(height,rank[i]+1,rank[j])
=
R
M
Q
(
h
e
i
g
h
t
,
r
a
n
k
[
i
]
+
1
,
r
a
n
k
[
j
]
)
这个结论的证明参考了上面提到的那篇博客,大家可以去看看那一位dalao的详细证明思路。证明如下:
后缀数组要发挥威力,还必须依靠一个很重要的结论:
证明大家可以看看论文或者博客。我这里让大家【感性认知】一下。
下面是一个例子,大家可以对照着验证上面两个结论。
下面是博主求height数组的参考实现代码。
void CalHeight(char *arr, int n) {
int k = 0;
for(int i=0;i<n;i++) {
if( rnk[i] == 0 ) height[rnk[i]] = 0;
else {
if( k ) k--;
int j = sa[rnk[i]-1];
while( arr[i+k] == arr[j+k] )
k++;
height[rnk[i]] = k;
}
}
}
5.后缀数组例题
暂缺,待填坑。
6.更新日志
UPD in 2018/8/12:对讲解部分进行了完善修改。
UPD in 2018/8/12:我想了想,例题大家还是看看dalao的论文吧,里面的例题非常丰富来着。如果之后我有遇到什么后缀数组的题的话,就加在这个地方