后缀数组 (SA)
约定
- 本文中字符串下标从1开始
- 后缀\(i\)表示\(s[i\dots n]\)
简述
后缀数组(Suffix Array)主要由两个数组组成:\(sa\)和\(rank\)。
其中\(sa_i\)表示排名为\(i\)的后缀,\(rank_i\)表示后缀\(i\)的排名。其二者构成了双映射的关系,即:\(sa_{rank_i}=i,rank_{sa_i}=i\)。
由于我们知道子串可以视作某个后缀的前缀,所以有时会在字符串的题目中利用此性质用到后缀数组。
\(O(nlog^2n)\)解法
若一次比较的复杂度是\(O(1)\)的,显然直接可以利用\(sort\)获得\(O(nlogn)\)的排序复杂度。但对于两个字符串无法实现比较的\(O(1)\)。回想字符串的比较方式,先找到两者的\(lcp\)(最长公共前缀),再比较下一位的大小即可(下一位为空视为最小)。也就是说,只要有较为快速的方法找到\(lcp\),就可以快速地进行比较。此时可以利用二分的方法,用哈希值判断是否相等,\(O(log\ n)\)地找到两者的\(lcp\)并实现大小的比较。于是整体的排序复杂度即为\(O(nlog^2n)\)
有可能遇上哈希冲突的问题,但此种方法胜在好写。
\(O(nlogn)\)解法
这是一种基于倍增思想的做法。
先对每个长度为1的子串(即每个字符)进行排序。
当已知长度为\(w\)的所有子串排名情况时,对于长度为\(2w\)的所有子串,其比较就是一个双关键字的比较。即先比较长度为\(w\)的前缀,若相等再比较后缀。超出\(n\)的部分统一视为无穷小,不会影响原子串的大小判断。等到\(w\)足够大\((\ge n-1)\)时,由于\(s[i\dots i+w]\)与后缀\(i\)的排名相同,最后得到的\(sa_i\)即为所求。
假设每次对长度为\(w\)的排序都可以实现\(O(n)\)的时间复杂度,则总体便可以达到\(O(nlogn)\)。因为在此背景下值域为排名,与\(n\)同阶,可以直接采用内层套用计数排序的基数排序实现\(O(n)\)的排序。