后缀数组

参考:https://www.bilibili.com/video/av92589768?from=search&seid=11036159274843024348

符号

子串

从原串中选取连续的一段即为子串,空串也是子串

后缀

我们用 s u f ( k ) suf(k) suf(k)表示 s ( k … n ) s(k…n) s(kn)构成的子串

任何子串都是某个后缀的前缀

最长公共前缀lcp

l c p ( s u f ( i ) , s u f ( j ) ) lcp(suf(i), suf(j)) lcp(suf(i),suf(j)) 表示两个串 s u f ( i ) suf(i) suf(i) s u f ( j ) suf(j) suf(j)最长的一样的前缀

问题

将所有后缀 s u f ( 1 ) , s u f ( 2 ) , … , s u f ( N ) suf(1),suf(2),…,suf(N) suf(1),suf(2),suf(N)按照字典序从小到大排序

方法1

首先看到题目想到的就是直接用暴力,建一个 c m p cmp cmp数组,用 s t r i n g string string可以比较大小的性质去暴力 s o r t sort sort

因为 s o r t sort sort n log ⁡ n n\log n nlogn的,每次 c m p cmp cmp函数都是 O ( n ) O(n) O(n)的,所以总的时间复杂度就是 n 2 log ⁡ n n^2\log n n2logn

方法2

想一想更好的做法,我们可以用二分+hash

复杂度: n log ⁡ 2 n n \log^2n nlog2n

c m p cmp cmp函数中二分 s u f ( i ) suf(i) suf(i) s u f ( j ) suf(j) suf(j) l c p lcp lcp

r e t u r n   s [ i + ∣ l c p ∣ ] < s [ j + ∣ l c p ∣ ] return\ s[i + |lcp|] < s[j +|lcp|] return s[i+lcp]<s[j+lcp]

方法3

S A SA SA算法

$SA[l] = $ 排名第 l l l的后缀的开始位置

$Rank[i] = $ 后缀 s u f ( i ) suf(i) suf(i)的排名

Rank[SA[l]] = l;
SA[Rank[i]] = i;

求出其中一个就能 O ( n ) O(n) O(n)求出另一个

有什么求其中一个数组的好的方法呢?

答案是倍增

方法三实现优化

倍增

s u b [ i ] [ k ] = s sub[i][k] = s sub[i][k]=s i i i开始长度 = s k =s^k =sk的子串

s u b [ i ] [ k ] = s [ i … i + ( 1 < < k ) − 1 ] sub[i][k]=s[i…i+(1 << k) - 1] sub[i][k]=s[ii+(1<<k)1],超过 n n n的部分都视为**’\0’**(字典序最小的字符)

r a n k [ i ] [ k ] = s u b [ i ] [ k ] rank[i][k] = sub[i][k] rank[i][k]=sub[i][k]在长度 = 2 k =2^k =2k的所有子串中的排名

$sa[l][k] = 在 长 度 在长度 =2^k 的 所 有 子 串 中 排 名 第 的所有子串中排名第 l$的子串的开始位置

过程
  1. 求出 s u b [ 1 ] [ 0 ] , s u b [ 2 ] [ 0 ] , … , s u b [ n ] [ 0 ] sub[1][0], sub[2][0], …,sub[n][0] sub[1][0],sub[2][0],sub[n][0]的字典排序
  2. 求出 s u b [ 1 ] [ 1 ] , s u b [ 2 ] [ 1 ] , … , s u b [ n ] [ 1 ] sub[1][1], sub[2][1], …,sub[n][1] sub[1][1],sub[2][1],sub[n][1]的字典排序
  3. ……
  4. 求出 s u b [ 1 ] [ k ] , s u b [ 2 ] [ k ] , … , s u b [ n ] [ k ] sub[1][k], sub[2][k],…,sub[n][k] sub[1][k],sub[2][k],sub[n][k]的字典排序

当子串长度 2 k > = n 2^k>=n 2k>=n时,子串排序就是后缀排序

利用 r a n k [ 1 … n ] [ k ] rank[1…n][k] rank[1n][k],如何求出 r a n k [ 1 … n ] [ k + 1 ] rank[1…n][k+1] rank[1n][k+1]

对于两个子串 s u b [ i ] [ k + 1 ] sub[i][k+1] sub[i][k+1] s u b [ j ] [ k + 1 ] sub[j][k+1] sub[j][k+1]

先比较 r a n k [ i ] [ k ] < r a n k [ j ] [ k ] rank[i][k]<rank[j][k] rank[i][k]<rank[j][k]

若相等,再比较 r a n k [ i + 2 k ] [ k ] < r a n k [ j + 2 k ] [ k ] rank[i+2^k][k]<rank[j+2^k][k] rank[i+2k][k]<rank[j+2k][k]

其实就相当于对二元组 ( r a n k [ i ] [ k ] , r a n k [ i + 2 k ] [ k ] ) (rank[i][k], rank[i+2^k][k]) (rank[i][k],rank[i+2k][k])排序

p a i r pair pair排序时,先按 f i r s t first first比较,若相等再按 s e c o n d second second比较

但如果建 p a i r pair pair数组直接 s o r t sort sort的话,复杂度还是 n log ⁡ 2 n n\log^2n nlog2n,还不如写二分+hash

于是这个时候就出现了一个神奇的东西:基数排序

为什么可以优化呢?我们注意到 r a n k rank rank这个数组,他的值域是多少?

没错,值域就是不超过 n n n的正整数,所以我们就可以用基数排序,换句话说就是桶排序

关于基数排序的相关,看可以去看一下洛谷日报第十五期,这里给出链接:基数排序

S A SA SA时的基数排序用 c n t cnt cnt实现

如何将 a [ i ] a[i] a[i]数组基数排序,然后将结果放在 S A SA SA数组中呢?

下面的代码就实现了输入一个 a a a数组,得到 s a sa sa数组

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

比如一个 a a a数组为

a = [ 2 , 1 , 2 , 4 , 2 ] a=[2,1,2,4,2] a=[2,1,2,4,2]

若用 s a [ l ] sa[l] sa[l]表示排名第 l l l的数在 a a a中的下标

s a = [ 2 , 1 , 3 , 5 , 4 ] sa=[2,1,3,5,4] sa=[2,1,3,5,4]

就可以根据

Rank[SA[l]] = l;
SA[Rank[i]] = i;

得出 r a n k rank rank数组

r a n k = [ 2 , 1 , 2 , 3 , 2 ] rank=[2,1,2,3,2] rank=[2,1,2,3,2]

到这里我们就能回到一开始的问题,实现用 r a n k [ 1 … n ] [ k ] rank[1…n][k] rank[1n][k],如何求出 r a n k [ 1 … n ] [ k + 1 ] rank[1…n][k+1] rank[1n][k+1],步骤如下:

f o r ( k = 1 ∼ log ⁡ n ) \large for(k = 1 \sim \log n) for(k=1logn)

  • r a n k [ i + 2 k ] [ k ] rank[i+2^k][k] rank[i+2k][k](第二关键字)基数排序
  • r a n k [ i ] [ k ] rank[i][k] rank[i][k](第一关键字)基数排序,得到 s a [ i ] [ k + 1 ] sa[i][k+1] sa[i][k+1]数组
  • s a [ i ] [ k + 1 ] sa[i][k+1] sa[i][k+1]求出 r a n k [ i ] [ k + 1 ] rank[i][k+1] rank[i][k+1]

如果你细心的话可能会发现, k k k是从 1 1 1开始的而不是从 0 0 0开始的,那么 k k k 0 0 0时候怎么来的呢?

因为 2 0 2^0 20就是 1 1 1,所以我们可以直接把 r a n k rank rank数组(也就是排名)先设成当前字符的 ASCII \text{ASCII} ASCII码,这样就可以啦~

sa->rank

如果 r k [ i ] rk[i] rk[i]中有并列

for (int p = 0, i = 1; i <= n; i++) {
	if(oldrk[sa[i]] == oldrk[sa[i - 1]] && oldrk[sa[i] + k] == oldrk[sa[i - 1] + k])
		rk[sa[i]] = p;
	else rk[sa[i]] = ++p;
}
代码
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;

const int A = 1e6 + 11;

inline int read() {
	char c = getchar();
	int x = 0, f = 1;
	for ( ; !isdigit(c); c = getchar()) if (c == '-') f = -1;
	for ( ; isdigit(c); c = getchar()) x = x * 10 + (c ^ 48);
	return x * f;
}

char s[A];
int n, m, sa[A], rank[A], tp[A], tax[A];

void cntsort() {
	for (int i = 0; i <= m; i++) tax[i] = 0;
	for (int i = 1; i <= n; i++) tax[rank[i]]++;
	for (int i = 1; i <= m; i++) tax[i] += tax[i - 1];
	for (int i = n; i >= 1; i--) sa[tax[rank[tp[i]]]--] = tp[i];
}

void Sort() {
	m = 75;
	for (int i = 1; i <= n; i++) rank[i] = s[i] - '0' + 1, tp[i] = i;
	cntsort();
	for (int w = 1, p = 0; p < n; m = p, w <<= 1) {
		p = 0;
		for (int i = 1; i <= w; i++) tp[++p] = n - w + i;
		for (int i = 1; i <= n; i++) if(sa[i] > w) tp[++p] = sa[i] - w;
		cntsort();
		swap(tp, rank);
		rank[sa[1]] = p = 1;
		for (int i = 2; i <= n; i++) {
			rank[sa[i]] = (tp[sa[i - 1]] == tp[sa[i]] && tp[sa[i - 1] + w] == tp[sa[i] + w]) ? p : ++p;
		}
	}
}

int main() {
	scanf("%s", s + 1);
	n = strlen(s + 1);
	Sort();
	for(int i = 1; i <= n; i++) cout << sa[i] << ' ';
	return 0;
}

Height数组

我们通过求 S A SA SA数组可以把所有后缀排序,那么排序之后有啥用呢??

其实是为了快速的求出任意两个后缀的 l c p lcp lcp长度

我们记$Height[l] = 排 名 第 排名第 l-1 的 后 缀 和 排 名 第 的后缀和排名第 l 的 后 缀 的 的后缀的 lcp$长度

H e i g h t [ l ] = l c p ( s u f ( S A [ l − 1 ] , s u f ( S A [ l ] ) ) ) Height[l] = lcp(suf(SA[l-1], suf(SA[l]))) Height[l]=lcp(suf(SA[l1],suf(SA[l])))

H e i g h t [ 1 ] Height[1] Height[1]可以视作 0 0 0

假设 l = l= l=后缀 s u f ( i ) suf(i) suf(i)的排名,$r = 后 缀 后缀 suf(j) 的 排 名 ( 在 此 的排名(在此 l 不 一 定 小 于 不一定小于 r$,只是举例)

那么有结论:

  • l c p ( s u f ( i ) , d u f ( j ) ) = m i n ( H e i g h t [ l + 1 ] … H e i g h t [ r ] ) lcp(suf(i),duf(j))=min(Height[l+1]…Height[r]) lcp(suf(i),duf(j))=min(Height[l+1]Height[r])
  • 即两个后缀的 l c p = lcp= lcp=它们排名区间中 H e i g h t Height Height的最小值

可以用数据结构维护 r m p rmp rmp

为什么可以这么理解呢?

假设有三个字符串 s 1 , s 2 , s 3 s_1,s_2,s_3 s1,s2,s3,且 s 1 < s 2 < s 3 s_1<s_2<s_3 s1<s2<s3(按 r a n k rank rank排名得出)

那么 l c p ( s 1 , s 3 ) lcp(s_1,s_3) lcp(s1,s3)就等于 m i n ( l c p ( s 1 , s 2 ) , l c p ( s 2 , s 3 ) ) min(lcp(s_1,s_2), lcp(s_2,s_3)) min(lcp(s1,s2),lcp(s2,s3))

(详细证明需要画图……我真的懒)

l c p ( s 1 , s 3 ) > = m i n ( l c p ( s 1 , s 2 ) , l c p ( s 2 , s 3 ) ) = 1 lcp(s_1,s_3) >= min(lcp(s_1,s_2), lcp(s_2,s_3))=1 lcp(s1,s3)>=min(lcp(s1,s2),lcp(s2,s3))=1

又有 s 1 [ l + 1 ] ! = s 3 [ l + 1 ] s_1[l+1]!= s_3[l+1] s1[l+1]!=s3[l+1]

求法

那么如何快速求出 H e i g h t Height Height数组呢?

纯暴力 O ( n 2 ) O(n^2) O(n2)
for i = 1 - N
	l = rank[i]
	j = sa[l - 1]
	k = 0
	while (s[i + k] ==s [j + k]): ++k
	Height[l] = k

l = r a n k [ i ] , r = r a n k [ i − 1 ] l = rank[i], r = rank[i-1] l=rank[i],r=rank[i1]
H e i g h t [ l ] = l c p ( s u f ( S A [ l − 1 ] ) , s u f ( i ) ) Height[l] = lcp(suf(SA[l-1]), suf(i)) Height[l]=lcp(suf(SA[l1]),suf(i))
H e i g h t [ r ] = 1 c p ( s u f ( S A [ r − 1 ] ) , s u f ( i − 1 ) ) Height[r] = 1cp(suf(SA[r-1]),suf(i-1)) Height[r]=1cp(suf(SA[r1])suf(i1))

有重要结论:
H e i g h t [ l ] > = H e i g h t [ r ] − 1 Height[l] >= Height[r] - 1 Height[l]>=Height[r]1

  • H e i g h t [ r ] > 1 Height[r]>1 Height[r]>1,有 s u f ( S A [ r − 1 ] ) < s u f ( S A [ i − 1 ] ) suf(SA[r-1]) < suf(SA[i-1]) suf(SA[r1])<suf(SA[i1])
  • 去掉首个字符 l c p ( s u f ( S A [ r − 1 ] + 1 ) , s u f ( S A [ i ] ) ) = H e i g h t [ r ] − 1 lcp(suf(SA[r-1]+1), suf(SA[i])) = Height[r] - 1 lcp(suf(SA[r1]+1)suf(SA[i]))=Height[r]1
  • s u f ( S A [ r − 1 ] + 1 ) < s u f ( S A [ i ] ) suf(SA[r-1]+1) < suf(SA[i]) suf(SA[r1]+1)<suf(SA[i])
  • 由于$Height[1] 是 是 suf(i) 与 排 名 紧 挨 着 自 己 的 后 缀 的 与排名紧挨着自己的后缀的 lcp$,有
  • s u f ( S A [ r − 1 ] + 1 ) < = s u f ( S A [ 1 − 1 ] ) < s u f ( S A [ i ] ) suf(SA[r-1]+1) <= suf(SA[1-1]) < suf(SA[i]) suf(SA[r1]+1)<=suf(SA[11])<suf(SA[i])

相近的 H e i g h t Height Height会比较相似,比较远的会差别很大

不恰当的例子:

优化 O ( n ) O(n) O(n)

利用 H e i g h t [ r a n k [ i ] ] > = H e i g h t [ r a n k [ i − 1 ] ] − 1 Height[rank[i]] >= Height[rank[i-1] ] - 1 Height[rank[i]]>=Height[rank[i1]]1
优化暴力即可,复杂度 0 ( N ) 0(N) 0(N)

for i = 1 - N
	j = sa[l - 1]
	k = max(0, Height[rank[i - 1]] - 1)
	while (s[i + k] == S[j+k]): ++k
	Height[rank[i]] = k

之后再用 s t st st表之类的维护 H e i g h t Height Height r m q rmq rmq信息即可

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值