数据结构后缀数组

Posted stelayuri

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构后缀数组相关的知识,希望对你有一定的参考价值。


后缀数组 Suffix Array

  对于一个长度为 len 字符串 S ,将其 len 个后缀根据字典序排序得到的排名数组即为后缀数组

  suff[i]:表示 S 中起始位置为 i 的后缀子串(第 i 长的后缀)

  在求解的过程中还会维护三个数组 sa / rk / height

  sa[i]:表示排名为 i 的后缀(起始位置的)下标

  rk[i]:表示下标为 i 的后缀排名

  height[i]:表示排名为 i 和 i-1 的后缀的最长公共前缀(长度)




含义

后缀子串

  例如对于字符串 abaabb,共有六个后缀子串

  abaabb / baabb / aabb / abb / bb / b

  则 suff 数组的内容为 {"abaabb","baabb","aabb","abb","bb","b"} (下标从1开始)

  将其按照字典序排序,得到

排名 后缀
1 aabb
2 abaabb
3 abb
4 b
5 baabb
6 bb

sa数组

  sa 数组表示排名为 i 的后缀(起始位置的)下标

  所以得到上述 suff 数组与后缀数组后

  sa[i] 就等于后缀数组第 i 个子串在 suff 数组中的位置

  例如排名为 1 的子串 aabb,在 suff 数组中位于第三个位置,则 sa[1] = 3

  也可以根据 suff 数组内子串长度 = len-i+1 的性质,直接计算出 aabb 所在位置为 6-4+1=3

  依此可以构造出 sa 数组的内容为 {3,1,4,6,2,5} (下标从1开始)


rk数组

  rk 数组与 sa 数组互逆,表示下标为 i 的后缀的排名

  所以得到上述 suff 数组与后缀数组后

  rk[i] 就等于 suff 数组第 i 个子串在后缀数组中的位置

  例如第 1 个位置的 abaabb,在后缀数组中排名为 2 ,则 rk[1]=2

  依此可以构造出 rk 数组的内容为 {2,5,1,3,6,4} (下标从1开始)


height数组

  由定义可得 height[i] = LCP( suff[sa[i]] , suff[sa[i-1]] ) ,( i>1 且 i≤len )

  例如当 i=2 时,height[2] 表示的则是 SA[1] 和 SA[2] 的最长公共前缀长度

  可得 SA[1] = "aabb" , SA[2] = "abaabb"

  公共前缀为 "a",故长度为 1,height[2]=1

  依此可以得到 height 数组的内容为 {0,1,2,0,1,1} (下标从1开始,height[1] 固定为0)




功能实现

求sa数组的方式

快速排序

  快排时间复杂度为O(nlogn),而字符串对比则是从前往后依次对比,复杂度为O(n)

  故此方法总时间复杂度为O(n2logn)

DA倍增算法

  Doubling Algorithm 模板

  先根据字符串中字符的出现情况,给每一种字符一个对应的排名(从1开始),作为第一次排序的结果

  其后每一次,每个位置以当前排名作为主关键词,从1开始倍增步数,将对应的位置排名作为第二关键词

  于是根据主关键词与副关键词继续给定排名,作为当次排序的结果

  如果加上倍增的步数后超出了字符串长度Len,则副关键词排名为 0

  如此循环,直到第一个位置加上倍增步数后超出字符串长度为止,算作算法结束,此时得到的排序结果即为sa数组

  总共排序次数为 logn

  若排序使用快排O(nlogn),则总时间复杂度为O(nlog2n)

  若使用基数排序,则可将排序复杂度降至O(n),总复杂度降为O(nlogn)

DC3算法

  一种常数很大但时间复杂度仅为O(n)的算法(保留)


根据互逆性质 求rk数组

  由于 rk 数组与 sa 数组存在互逆性质( rk[sa[i]] == i && sa[rk[i]] == i

  所以可以直接从 1 开始进行 O(n) 的枚举求出 rk 数组


根据关系 求height数组

  由于 height[i] = LCP( suff[sa[i]] , suff[sa[i-1]] )

  若暴力对比求解LCP,时间复杂度是O(n2)

  关于 height 数组,存在一条必然关系 height[rk[i]] >= height[rk[i-1]] - 1

  所以根据这条关系,只需要 i 从小到大枚举,O(n) 对比一遍字符串即可求出整个 height 数组

  (即求出height[rk[i-1]]后,以height[rk[i-1]]-1作为LCP的最小值,继续进行比较)




常规用法

求同一字符串内两后缀的最长公共前缀长度

  由于后缀数组是由有序的后缀构成的

  且 height 数组表示后缀数组中相邻两字符串的最长公共前缀

  获得两后缀在后缀数组内的排名后(假设为sa[x]与sa[y],sa[x]<sa[y])

  则最长公共前缀长度为 min{height[i]} , sa[x]+1 <= i <= sa[y]

int mxLCP=0;
for(int i=sa[x]+1;i<=sa[y];i++)
    mxLCP=max(mxLCP,height[i]);

求同一字符串内两子串的最长公共前缀长度

  与上一个例子不同的是,子串不是后缀

  但是可以将子串从字符串内找出,加上缺少的部分使其成为后缀

  同样的,先找出两后缀的最长公共前缀长度 min{height[i]} , sa[x]+1 <= i <= sa[y]

  然后再考虑找出的长度与两子串的关系,即最长公共前缀长度不可能大于其中任意一个

  所以再与两子串长度取小即可

int mxLCP=0;
for(int i=sa[x]+1;i<=sa[y];i++)
    mxLCP=max(mxLCP,height[i]);
mxLCP=min(mxLCP,min(xlen,ylen));

求可重叠的最长重复子串长度

  由后缀数组性质可以得知,与某个后缀的公共前缀最长的后缀一定是在后缀数组中相邻的两个后缀内(如果存在)

  所以就是问height数组的最大值 max{height[i]} , 2 <= i <= len

int mxLen=0;
for(int i=2;i<=len;i++)
    mxLen=max(mxLCP,height[i]);

求不可重叠的最长重复子串长度

  首先抓住一个结论:对于后缀数组排名为x与y的两后缀,设它们的最长公共前缀长度为 L

  则 height[i] >= L 在 i = x+1 ~ y 中始终成立

  对于这个 L,可以通过二分而得,每次二分进行一次判断

  每次判断遍历一遍height数组,将连续的 height[i] >= L 的位置看作一条线段

  如果线段内包括的后缀数量大于L(线段内元素个数大于等于L),则说明这条线段两端的后缀的最长公共前缀即为所求(存在)

  二分找到最大的L即可

bool binarySearch(int L)
{
    int pos=0;
    for(int i=2;i<=len;i++)
    {
        if(height[i]>=L)
        {
            if(!pos)
                pos=i; //记录左边界
        }
        else
        {
            if(pos)
            {
                if(i-pos+1>=L)
                    return true;
            }
            pos=0;
        }
    }
    if(pos)
    {
        if(len-pos+1>=L)
            return true;
    }
    return false;
}

求字符串内不同子串数量

  因为已经对后缀做了处理,则子串可以从每个后缀的前缀中得到

  对于后缀数组中排名为 i 的后缀,其长度可由 len-sa[i]+1 得到

  这个后缀所能得到的前缀数量也就是 len-sa[i]+1

  对于可能出现的重复计数,只需要通过相邻两后缀最长公共前缀 height[i] 来计算重复的数量即可

  除了排名为 1 的后缀以 len-sa[i]+1 计算,其余均为 len-sa[i]+1-height[i]

int cnt=len-sa[1]+1;
for(int i=2;i<=len;i++)
    cnt+=len-sa[i]+1-height[i];

求可重叠的至少出现k次的最长子串长度

  同样二分长度L,再将符合条件的连续 height 视作线段

  判断线段内 height 数量是否满足 count+1 >= k 即可

bool binarySearch(int L)
{
    int pos=0;
    for(int i=2;i<=len;i++)
    {
        if(height[i]>=L)
        {
            if(!pos)
                pos=i; //记录左边界
        }
        else
        {
            if(pos)
            {
                if(i-pos+1>=k)
                    return true;
            }
            pos=0;
        }
    }
    if(pos)
    {
        if(len-pos+1>=k)
            return true;
    }
    return false;
}

以上是关于数据结构后缀数组的主要内容,如果未能解决你的问题,请参考以下文章

后缀数组之倍增算法

VSCode自定义代码片段—— 数组的响应式方法

VSCode自定义代码片段10—— 数组的响应式方法

_bzoj1031 [JSOI2007]字符加密Cipher后缀数组

Sublime Text3自定义代码片段

后缀数组代码详解