后缀数组简介
一些约定
字符串相关的定义请参考 字符串基础。
字符串下标从 \(1\) 开始。
字符串 \(s\) 的长度为 \(n\)。
" 后缀 \(i\)" 代指以第 \(i\) 个字符开头的后缀,存储时用 \(i\) 代表字符串 \(s\) 的后缀 \(s[i\dots n]\)。
后缀数组是什么?
后缀数组(Suffix Array)主要关系到两个数组:\(sa\) 和 \(rk\)。
其中,\(sa[i]\) 表示将所有后缀排序后第 \(i\) 小的后缀的编号,也是所说的后缀数组,后文也称编号数组 \(sa\);
\(rk[i]\) 表示后缀 \(i\) 的排名,是重要的辅助数组,后文也称排名数组 \(rk\)。
这两个数组满足性质:\(sa[rk[i]]=rk[sa[i]]=i\)。
解释
后缀数组示例:
[][2]
后缀数组怎么求?
O(n^2logn) 做法
相信这个做法大家还是能自己想到的:将盛有全部后缀字符串的数组进行 sort
排序,由于排序进行 \(O(n\log n)\) 次字符串比较,每次字符串比较要 \(O(n)\) 次字符比较,所以这个排序是 \(O(n^2\log n)\) 的时间复杂度。
O(nlog^2n) 做法
这个做法要用到倍增的思想。
首先对字符串 \(s\) 的所有长度为 \(1\) 的子串,即每个字符进行排序,得到排序后的编号数组 \(sa_1\) 和排名数组 \(rk_1\)。
倍增过程:
-
用两个长度为 \(1\) 的子串的排名,即 \(rk_1[i]\) 和 \(rk_1[i+1]\),作为排序的第一第二关键字,就可以对字符串 \(s\) 的每个长度为 \(2\) 的子串:\(\{s[i\dots \min(i+1, n)]\ |\ i \in [1,\ n]\}\) 进行排序,得到 \(sa_2\) 和 \(rk_2\);
-
之后用两个长度为 \(2\) 的子串的排名,即 \(rk_2[i]\) 和 \(rk_2[i+2]\),作为排序的第一第二关键字,就可以对字符串 \(s\) 的每个长度为 \(4\) 的子串:\(\{s[i\dots \min(i+3, n)]\ |\ i \in [1,\ n]\}\) 进行排序,得到 \(sa_4\) 和 \(rk_4\);
-
以此倍增,用长度为 \(w/2\) 的子串的排名,即 \(rk_{w/2}[i]\) 和 \(rk_{w/2}[i+w/2]\),作为排序的第一第二关键字,就可以对字符串 \(s\) 的每个长度为 \(w\) 的子串 \(s[i\dots \min(i+w-1,\ n)]\) 进行排序,得到 \(sa_w\) 和 \(rk_w\)。其中,类似字母序排序规则,当 \(i+w>n\) 时,\(rk_w[i+w]\) 视为无穷小;
-
\(rk_w[i]\) 即是子串 \(s[i\dots i + w - 1]\) 的排名,这样当 \(w \geqslant n\) 时,得到的编号数组 \(sa_w\),也就是我们需要的后缀数组。
过程
倍增排序示意图:
[][2]
显然倍增的过程是 \(O(\log n)\),而每次倍增用 sort
对子串进行排序是 \(O(n\log n)\),而每次子串的比较花费 \(2\) 次字符比较;
除此之外,每次倍增在 sort
排序完后,还有额外的 \(O(n)\) 时间复杂度的,更新 \(rk\) 的操作,但是相对于 \(O(n\log n)\) 被忽略不计;
所以这个算法的时间复杂度就是 \(O(n\log^2n)\)。
实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
|
O(nlogn) 做法
在刚刚的 \(O(n\log^2n)\) 做法中,单次排序是 \(O(n\log n)\) 的,如果能 \(O(n)\) 排序,就能 \(O(n\log n)\) 计算后缀数组了。
由于计算后缀数组的过程中排序的关键字是排名,值域为 \(O(n)\),并且是一个双关键字的排序,可以使用基数排序优化至 \(O(n)\)。
实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
|
一些常数优化
如果你把上面那份代码交到 LOJ #111: 后缀排序 上:
这是因为,上面那份代码的常数的确很大。
第二关键字无需计数排序
思考一下第二关键字排序的实质,其实就是把超出字符串范围(即 \(sa[i] + w > n\))的 \(sa[i]\) 放到 \(sa\) 数组头部,然后把剩下的依原顺序放入:
1 2 3 4 |
|
优化计数排序的值域
每次对 \(rk\) 进行更新之后,我们都计算了一个 \(p\),这个 \(p\) 即是 \(rk\) 的值域,将值域改成它即可。
若排名都不相同可直接生成后缀数组
考虑新的 \(rk\) 数组,若其值域为 \([1,n]\) 那么每个排名都不同,此时无需再排序。
实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
|
O(n) 做法
在一般的题目中,常数较小的倍增求后缀数组是完全够用的,求后缀数组以外的部分也经常有 \(O(n\log n)\) 的复杂度,倍增求解后缀数组不会成为瓶颈。
但如果遇到特殊题目、时限较紧的题目,或者是你想追求更短的用时,就需要学习 \(O(n)\) 求后缀数组的方法。
SA-IS
可以参考 诱导排序与 SA-IS 算法,另外它的 评论页面 也有参考价值。
DC3
可以参考[[2009] 后缀数组——处理字符串的有力工具 by. 罗穗骞][2]。
后缀数组的应用
寻找最小的循环移动位置
将字符串 \(S\) 复制一份变成 \(SS\) 就转化成了后缀排序问题。
例题:「JSOI2007」字符加密。
在字符串中找子串
任务是在线地在主串 \(T\) 中寻找模式串 \(S\)。在线的意思是,我们已经预先知道知道主串 \(T\),但是当且仅当询问时才知道模式串 \(S\)。我们可以先构造出 \(T\) 的后缀数组,然后查找子串 \(S\)。若子串 \(S\) 在 \(T\) 中出现,它必定是 \(T\) 的一些后缀的前缀。因为我们已经将所有后缀排序了,我们可以通过在 \(p\) 数组中二分 \(S\) 来实现。比较子串 \(S\) 和当前后缀的时间复杂度为 \(O(|S|)\),因此找子串的时间复杂度为 \(O(|S|\log |T|)\)。注意,如果该子串在 \(T\) 中出现了多次,每次出现都是在 \(p\) 数组中相邻的。因此出现次数可以通过再次二分找到,输出每次出现的位置也很轻松。
从字符串首尾取字符最小化字典序
题意:给你一个字符串,每次从首或尾取一个字符组成字符串,问所有能够组成的字符串中字典序最小的一个。
题解
暴力做法就是每次最坏 \(O(n)\) 地判断当前应该取首还是尾(即比较取首得到的字符串与取尾得到的反串的大小),只需优化这一判断过程即可。
由于需要在原串后缀与反串后缀构成的集合内比较大小,可以将反串拼接在原串后,并在中间加上一个没出现过的字符(如 #
,代码中可以直接使用空字符),求后缀数组,即可 \(O(1)\) 完成这一判断。
参考代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
|
height 数组
LCP(最长公共前缀)
两个字符串 \(S\) 和 \(T\) 的 LCP 就是最大的 \(x\)(\(x\le \min(|S|, |T|)\)) 使得 \(S_i=T_i\ (\forall\ 1\le i\le x)\)。
下文中以 \(lcp(i,j)\) 表示后缀 \(i\) 和后缀 \(j\) 的最长公共前缀(的长度)。
height 数组的定义
\(height[i]=lcp(sa[i],sa[i-1])\),即第 \(i\) 名的后缀与它前一名的后缀的最长公共前缀。
\(height[1]\) 可以视作 \(0\)。
O(n) 求 height 数组需要的一个引理
\(height[rk[i]]\ge height[rk[i-1]]-1\)
证明
当 \(height[rk[i-1]]\le1\) 时,上式显然成立(右边小于等于 \(0\))。
当 \(height[rk[i-1]]>1\) 时:
根据 \(height\) 定义,有 \(lcp(sa[rk[i-1]], sa[rk[i-1]-1]) = height[rk[i-1]] > 1\)。
既然后缀 \(i-1\) 和后缀 \(sa[rk[i-1]-1]\) 有长度为 \(height[rk[i-1]]\) 的最长公共前缀,
那么不妨用 \(aA\) 来表示这个最长公共前缀。(其中 \(a\) 是一个字符,\(A\) 是长度为 \(height[rk[i-1]]-1\) 的字符串,非空)
那么后缀 \(i-1\) 可以表示为 \(aAD\),后缀 \(sa[rk[i-1]-1]\) 可以表示为 \(aAB\)。(\(B < D\),\(B\) 可能为空串,\(D\) 非空)
进一步地,后缀 \(i\) 可以表示为 \(AD\),存在后缀(\(sa[rk[i-1]-1]+1\))\(AB\)。
因为后缀 \(sa[rk[i]-1]\) 在大小关系的排名上仅比后缀 \(sa[rk[i]]\) 也就是后缀 \(i\),小一位,而 \(AB < AD\)。
所以 \(AB \leqslant\) 后缀 \(sa[rk[i]-1] < AD\),显然后缀 \(i\) 和后缀 \(sa[rk[i]-1]\) 有公共前缀 \(A\)。
于是就可以得出 \(lcp(i,sa[rk[i]-1])\) 至少是 \(height[rk[i-1]]-1\),也即 \(height[rk[i]]\ge height[rk[i-1]]-1\)。
O(n) 求 height 数组的代码实现
利用上面这个引理暴力求即可:
1 2 3 4 5 6 |
|
\(k\) 不会超过 \(n\),最多减 \(n\) 次,所以最多加 \(2n\) 次,总复杂度就是 \(O(n)\)。
height 数组的应用
两子串最长公共前缀
\(lcp(sa[i],sa[j])=\min\{height[i+1..j]\}\)
感性理解:如果 \(height\) 一直大于某个数,前这么多位就一直没变过;反之,由于后缀已经排好序了,不可能变了之后变回来。
严格证明可以参考[[2004] 后缀数组 by. 许智磊][1]。
有了这个定理,求两子串最长公共前缀就转化为了 RMQ 问题。
比较一个字符串的两个子串的大小关系
假设需要比较的是 \(A=S[a..b]\) 和 \(B=S[c..d]\) 的大小关系。
若 \(lcp(a, c)\ge\min(|A|, |B|)\),$A
本页面最近更新:,更新历史
发现错误?想一起完善? 在 GitHub 上编辑此页!
本页面贡献者:OI-wiki
本页面的全部内容在 CC BY-SA 4.0 和 SATA 协议之条款下提供,附加条款亦可能应用