Suffix Array 后缀数组算法心得

  后缀数组(suffix array) 在网上有不少讲解,但没有哪篇博客写的比较详细。所以,我决定以我对后缀数组的理解来写一篇博客。

  后缀数组(以下简称为SA)本身是后缀树的一种替代用结构,本质上来讲其实是存储了后缀树上的一些重要信息。由于后缀树实现较为繁琐故竞赛多采用后缀数组。

  由于SA本身产生于后缀树,所以我们从后缀树说起。

  首先是后缀的定义:

    suffix(i) : 在一个字符串中从 i 位置开始到该串末尾结束的子串。

  以一个字符串的所有后缀为字符串集合建立一颗Trie树即可得到后缀树。

  后缀树是个比较好懂的概念。那么后缀树有什么作用呢?

  在讨论这个问题之前,我们需要先了解为什么要用后缀作为建树的依据。

  在我看来,用后缀处理问题的一个重要意义在于可以将子串的问题转化为后缀的前缀的问题。

  比如一个子串 s(i, j), 它本身就是suffix(i)这个字符串的prefix(j)(到 j 位置为止的前缀),那么对于子串的问题就可以转化为前缀的问题。

  说到这里,你可能还没有什么特殊的感觉。但是,当你把后缀建成Trie树之后,你就会发现,这后缀的前缀就是这树上某一深度下的路径,这样一来就成功的把子串的问题变成了树的问题。

  而后缀树的作用,就是为了简化处理子串问题的时间复杂度。

  以上的内容是我的个人理解。我自己觉得这些十分的重要,希望读者好好理解以上的概念。

  那么接下来,我们开始讲解SA 。

 

  之前说过, SA里面存储的是后缀树上的一些重要信息。其实,SA算法的思路很简单。我们都知道Trie树的构造是按照字典序来的, 即你对Trie进行先序遍历的话会得到所有后缀按字典序排序后的结果。

  而SA里面存储的就是这个先序遍历。

  由于在后缀树上,每一个后缀除了自身的位置以外,还有一个重要的信息存在--该后缀在原串中的位置, 所以后缀数组存储的其实是先序遍历后缀树(即给所有后缀排序)后各个后缀的彼此之间的顺序。

  所以现在我们定义 SA(i) 为 排名为 i 的后缀在原串中的位置(即起始位置下标)

  然后SA为了方便查找某后缀的排名,在此基础上定义了SA的逆运算 : Rank(i) : 以 i 位置开始的后缀在所有后缀中的排名。

  SA的构造算法很简单。由于它自身的目的就是给所有后缀排序,那么最无脑的办法就是使用STL里面的sort来排序, 复杂度O(n^2logn)

  观察后我们发现多的一层复杂度来自字符串比较。

  由于字符串比较的字符集大小较小(ASCII字符集最大127),所以我们使用基数排序来替代掉快排,这样我们就得到了O(nlogn)的算法

  理解了以上的东西, 接下来我们开始深入讲解SA的应用,这一部分也是重中之重。

  1. LCP问题

    看过SA相关资料的人大概都知道LCP吧。

    LCP:任意两个后缀之间的最长公共前缀。

    还记不记得之前说了什么?后缀的前缀就是子串。

    所以LCP是个子串问题的重要工具。

    直接考虑任意两个字符串的LCP该怎么办呢?

    在后缀树上来看,两个后缀的最长公共前缀…………不就是从树根出发的最长公共路径吗?

    最长公共路径不就是在LCA处分开的吗?

    所以两个后缀的最长公共前缀就可以直接转化为:树上两节点之间的LCA的深度。

    所以, LCP问题就是LCA问题。

    那么两点间的LCA的深度怎么求呢?

    不难发现两点的LCA的深度就是在树上两点之间的每个点与自己兄弟子树节点的LCA的深度的最小值(就是最离根最近的一点,这个很好证明也很好理解)

    于是我们又把LCP变成了RMQ。

    RMQ你得有个数组啊。

    所以我们需要求出相邻兄弟节点的LCA深度。

    也就是排名相邻的两点的LCP(它们两个之间没有点)

    而这个深度又叫

    height数组。

    估计这就是名字的由来吧。

    至此,LCP已解。

 2. 子串类问题

    这一部分有很多变式,我就挑几个比较典型的来说

    1. 最长可重叠子串长度

      这个问题等价于,树上最大的公共路径长度……

    2. 最长不可重叠子串长度

      这个问题需要考虑子串的位置。所以我们先把位置作为权值放在路径的起始节点上。这样一来问题就变成了在树上找一条最长的路径且最大权值差大于等于该路径长度。

      当给定长度找子串时, 我们可以考虑成给定深度找子树, 这样我们就可以以长度为依据给树分块,块内的height都大于该长度,所以它们都是可选的,这时我们只要看最大权值与最小权值之间的差距够不够就可以了。

      问题是没有给深度。 我们不难发现深度越小树上的块越大,权值差也就会越大。(极端情况深度为0,此时整棵树是一块),所以问题具有单调性,我们可以二分深度,然后给height分块(其实是给后缀树分块)

    3. 子串计数

      不重复子串有多少个。

      这也就是树的路径计数,只不过是到有标记的节点(后缀终点)的路径

      由于路径在LCA处分开,故从LCA位置开始是不同的子串。

      所以是SUM(n - sa[i] - height[i])

    4. 子串第 k 大

      数出小于suffix(sa[i])的子串的个数,二分

 

 

 

 

 

 

 

  

posted @ 2017-09-16 21:13  zzhzz  阅读(801)  评论(0编辑  收藏  举报