后缀数组SA-IS
S A − I S SA-IS SA−IS排序是基于诱导排序这种思想,将问题规模缩小,解决更小的问题,快速解决原问题的算法。
首先给出一些定义:
一 可能用到的
用大写字母表示字符串,小写字母表示字符或位置。
字符串最后用#表示。
在字符串
A
A
A中,
s
u
f
f
i
x
(
A
,
i
)
suffix(A,i)
suffix(A,i)表示长度为i的后缀。
二 后缀类型
在字符串
A
A
A中,对于每一个后缀,我们赋一个类型。
若
s
u
f
f
i
x
(
A
,
i
)
suffix(A,i)
suffix(A,i)为
S
S
S型后缀表示
s
u
f
f
i
x
(
A
,
i
)
<
s
u
f
f
i
x
(
A
,
i
+
1
)
suffix(A,i) < suffix(A,i+1)
suffix(A,i)<suffix(A,i+1)
若
s
u
f
f
i
x
(
A
,
i
)
suffix(A,i)
suffix(A,i)为
L
L
L型后缀表示
s
u
f
f
i
x
(
A
,
i
)
>
s
u
f
f
i
x
(
A
,
i
+
1
)
suffix(A,i) > suffix(A,i+1)
suffix(A,i)>suffix(A,i+1)
我们默认最后一个字符为
S
S
S型
后用t表示后缀类型数组。
三 LMS子串
我们定义*型为
S
S
S型,且它的左边为
L
L
L型,即它是一连串
S
S
S型的开头。
如上面的后缀类型比如以字符串
a
a
b
b
a
c
c
aabbacc
aabbacc为例:
— | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
S | a | a | b | b | a | c | c | # |
t | S | S | L | L | S | L | L | S |
— | * | * |
∗
*
∗型后缀如图所示。
则LMS子串表示两个*之间的子串
图上子串依次为:“aabba”,“acc#”,"#"。
利用所有的
∗
*
∗型后缀,我们得出一个新串
A
1
A1
A1
对于新串
A
1
A1
A1,
S
A
1
SA1
SA1是它们之间的相对顺序。
— | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
S | a | a | b | b | a | c | c | # |
t | S | S | L | L | S | L | L | S |
— | * | * | ||||||
A1 | — | — | — | — | 1 | — | — | 0 |
如图所示。
接下来给出一些引理:
引理一:
我们要考虑如何快速构造t数组。
首先有一些性质:
对于一个字符串
A
A
A一个后缀是类型为
S
S
S,当且仅当下面两项满足:
1.
A
[
i
]
<
A
[
i
+
1
]
A[i]<A[i+1]
A[i]<A[i+1]
2.
A
[
i
]
=
A
[
i
+
1
]
A[i]=A[i+1]
A[i]=A[i+1]且
t
[
i
+
1
]
t[i+1]
t[i+1]为
S
S
S型
同理,对于
L
L
L型:
1.
A
[
i
]
>
A
[
i
+
1
]
A[i]>A[i+1]
A[i]>A[i+1]
2.
A
[
i
]
=
A
[
i
+
1
]
A[i]=A[i+1]
A[i]=A[i+1]且
t
[
i
+
1
]
t[i+1]
t[i+1]为
L
L
L型
易证。
引理二:
若后缀
A
A
A为
S
S
S型,
B
B
B为
L
L
L型,当
A
[
0
]
=
B
[
0
]
A[0]=B[0]
A[0]=B[0],则
A
>
B
A>B
A>B
证明:
设
A
=
a
b
X
A=abX
A=abX,
B
=
a
c
Y
B=acY
B=acY。
假设
b
!
=
a
b!=a
b!=a且
c
!
=
a
c!=a
c!=a
因为
A
A
A为
S
S
S型,得:
a
<
b
a<b
a<b
同理:
a
>
c
a>c
a>c
则
b
>
c
b>c
b>c,所以
A
>
B
A>B
A>B
当
b
=
a
b=a
b=a或者
a
=
c
a=c
a=c时,也可以根据引理一,推成第一种情况。
引理三:
对于非"#"得
L
M
S
LMS
LMS子串长度大于2。
证明:两个
L
M
S
LMS
LMS串中间必定含有一个
L
L
L型后缀。
引理四:
一个字符串
A
A
A中的
L
M
S
LMS
LMS子串数量不超过
∣
A
∣
/
2
|A|/2
∣A∣/2。
证明:根据引理四,易证。
引理五:
对于一个后缀数组
S
A
SA
SA,对于一个相同字母开头的所有字符串中必定先排列
L
L
L型,再排列
S
S
S型。
证明:根据引理二可得。
一些过程:
一 诱导排序通过SA1得到SA
我们如果得出 S A 1 SA1 SA1,便可以利用引理五和诱导排序得出 S A SA SA,具体过程如下:
- 将 S A SA SA数组初始化为每个元素都为 − 1 -1 −1的数组。
- 确定每个桶 S S S型桶的起始位置。在 S A 1 SA1 SA1数组中从左往右扫一遍,按照相对顺序放进对应的桶里。
- 确定每个桶的 L L L型后缀相对顺序,在 S A SA SA中从左往右扫,若 S A [ i ] > 0 SA[i]>0 SA[i]>0,且 t [ S A [ i ] − 1 ] = L t[SA[i]-1]=L t[SA[i]−1]=L,则将 S A [ i ] − 1 SA[i]-1 SA[i]−1塞进对应的桶里。
- 确定每个桶的S型后缀相对顺序,在 S A SA SA中从右往左扫,若 S A [ i ] > 0 SA[i]>0 SA[i]>0,且 t [ S A [ i ] − 1 ] = S t[SA[i]-1]=S t[SA[i]−1]=S,则将 S A [ i ] − 1 SA[i]-1 SA[i]−1塞进对应的桶里。
注意:这里的桶是合起来形成一个大桶的,即
S
A
SA
SA,类似于基数排序的思想,提前算好每个桶的大小,故不会互相影响。
对应的桶指的是按首字母的编号形成的桶。
因为我们要确定L型后缀的起始位置,所以
S
S
S型桶是倒着放置的。
二 如何得到SA1
我们如果先对
L
M
S
LMS
LMS字串进行排序。
就可以得到每个
L
M
S
LMS
LMS子串的新序号。
若每个字符都不一样,则可直接得到
S
A
1
SA1
SA1,
否则递归求
S
A
1
SA1
SA1。
二 对LMS子串进行排序
我们仍然是使用诱导排序,但是要将第二步改为:
确定每个桶
S
S
S型桶的起始位置。将
L
M
S
LMS
LMS首字母按任意顺序放进对应的桶里。
这里我们引入一个概念
L
M
S
LMS
LMS的前缀函数。
在字符串
A
A
A中,如
p
r
e
(
A
,
i
)
pre(A,i)
pre(A,i),若
t
[
i
]
=
S
,
t
[
i
−
1
]
=
T
t[i]=S,t[i-1]=T
t[i]=S,t[i−1]=T,则
p
r
e
(
A
,
i
)
=
A
[
i
]
pre(A,i)=A[i]
pre(A,i)=A[i]
即它的
L
M
S
LMS
LMS前缀就是自己,否则是从
S
[
i
]
S[i]
S[i]开始到下一个
∗
*
∗型形成的字符串。
为什么按照随意顺序放可以使所有的前缀函数排序成功?
证明:
- 对于第二步,由于放入的 L M S LMS LMS前缀都只有一个字符,因为桶的排列是按照字典序的,所以保证放置后一定有序。
- 对于第三步,当放入第一个 L L L型 L M S LMS LMS前缀时, S A SA SA数组必定是有序的(根据引理二)。假设我们已经放置了 k k k个 L L L型 L M S LMS LMS前缀,且它们在 S A SA SA数组中保持有序,现在考虑放入的第 k + 1 k+1 k+1个 L M S LMS LMS前缀是否会保证有序。我们设这个 L M S LMS LMS前缀为 p r e ( S , i ) pre(S,i) pre(S,i),因为首字母不同的 L M S LMS LMS前缀一定是保持有序的,因此我们只需要考虑它与其首字母相同的 L M S LMS LMS前缀之间的关系。假设我们存在一个这样的 L M S LMS LMS前缀,使得 p r e ( S , j ) > p r e ( S , i ) pre(S,j)>pre(S,i) pre(S,j)>pre(S,i),由于 p r e ( S , j ) [ 0 ] = p r e ( S , i ) [ 0 ] pre(S,j)[0]=pre(S,i)[0] pre(S,j)[0]=pre(S,i)[0],所以我们得知 p r e ( S , j + 1 ) > p r e ( S , i + 1 ) pre(S,j+1)>pre(S,i+1) pre(S,j+1)>pre(S,i+1)。而 p r e ( S , j + 1 ) pre(S,j+1) pre(S,j+1)与 p r e ( S , i + 1 ) pre(S,i+1) pre(S,i+1)都是之前所加入过的,因此它们之间应当保持有序。而 p r e ( S , j ) > p r e ( S , i ) pre(S,j)>pre(S,i) pre(S,j)>pre(S,i)告诉我们之前的 S A SA SA数组不是有序的,与假设相反,故不存在这样的 p r e ( S , j ) pre(S,j) pre(S,j)。因此,当 p r e ( S , i ) pre(S,i) pre(S,i)放置后,,对于所有之前放置的且首字母与其相同的 L M S LMS LMS前缀 p r e ( S , j ) pre(S,j) pre(S,j),应该都有 p r e ( S , j ) < p r e ( S , i ) pre(S,j)<pre(S,i) pre(S,j)<pre(S,i), S A SA SA数组保持有序。
- 对于第四步的正确性的证明,与第三步的证明是类似的。读者可以自行推理一下。
时间复杂度
根据引理四可知,每次递归都会将长度缩小一半。
时间复杂度为:O(n)。
主要参考:https://riteme.github.io/blog/2016-6-19/sais.html#fn:star-in-S