完全弄懂后缀数组

Posted

tags:

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

什么叫后缀数组  首先要知道什么叫后缀

比如 字符串 abcdef  那么 abcdef bcdef cdef def ef f 就叫做后缀  也就是从最后一个字母之前的一个字母开始一直到最后一个字母  所构成的字符串就叫做后缀

至于后缀数组能干什么?我在这就不介绍了  我想你既然知道后缀数组就一定知道他的用处

但是自己之前读过很多后缀数组的文章  短短二三十代码  却没有找到一篇博客从头到尾讲解的

自己断断续续一个月终于算是对倍增算法(就是一个名字  不必纠结什么叫倍增算法)的有个比较深入理解

这是原始代码

int wa[maxn],wb[maxn],wv[maxn],ws[maxn];
 
int cmp(int *r , int a, int b, int l)
{
    return r[a] == r[b] && r[a+l] == r[b+l];
}
void da (int *r , int *sa , int n, int m)
{
    int i, j, p, *x = wa, *y = wb , *t;
    for(i = 0; i < m; i++) 
        ws[i] = 0;
    for(i = 0; i < n; i++) 
        ws[x[i] = r[i]]++;
    for(i = 1; i < m; i++) 
        ws[i] += ws[i-1];
    for(i = n-1; i >= 0; i--) 
        sa[--ws[x[i]]] = i;
    for(j = 1,p = 1; p < n ; j <<= 1,m = p)
    {
        for(p = 0, i = n - j; i < n; i++) 
            y[p++]=i;
        for(i = 0; i < n; i++)
            if(sa[i] >= j)
                y[p++] = sa[i] - j;
        for(i = 0; i < n; i++)
            wv[i] = x[y[i]];
        for(i = 0; i < m; i++)
            ws[i] = 0;
        for(i = 0; i < n; i++)
            ws[wv[i]]++;
        for(i = 1; i < m; i++)
            ws[i] += ws[i-1];
        for(i = n-1; i >= 0; i--)
            sa[--ws[wv[i]]] = y[i];
        for(t = x,x = y,y = t,p = 1,x[sa[0]] = 0,i = 1; i < n;i++)
            x[sa[i]]=cmp(y,sa[i-1],sa[i],j)?p-1:p++;
    }
}

要想了解上面的代码  首先你要知道什么叫基数排序(基数排序 百度百科

假设你也已经了解了了基数排序  那么下面我们就要解析上面的代码

还有在这里你首先要知道

后缀数组(SA[i]存放排名第i大的子串首字符下标)

  后缀数组 SA 是一个一维数组,它保存1..n 的某个排列 SA[1] ,SA[2] , ……, SA[n] ,并且保证Suffix(SA[i])<Suffix(SA[i+1]), 1 ≤ i<n 。也就是将 S 的 n 个后缀从小到大进行排序之后把排好序的后缀的开头位置顺次放入SA 中。

名次数组(rank[i]存放suffix(i)的优先级)

  名次数组 Rank[i] 保存的是 Suffix(i) 在所有后缀中从小到大排列的 “ 名次 ” 。

最后总结为  SA[i] = j表示为按照从小到大排名为i的后缀  是以j(下标)开头的后缀

rank[i] = j 表示为按照从小到大排名  以i为下标开始的后缀  排名为j

RANK表示你排第几   SA表示排第几的是谁

下面的这张图就是上面算法的思想  但是我当时看的时候  晕了

技术分享

下面我们一步步来 

首先 不管什么算法 我们来点暴力的 假设现在 我们直接来求一个字符串所有后缀的大小  你会怎么做

两个for循环比较呗  但是这样算法肯定慢

int smpStr(char* str,int len){

  int k=0;

  for(int i=0;i<len;i++){

    for(int j=i;j<len;j++){

      if(strcmp(str+k,str+j)>0){

        k = j;
      }

    }
    rank[k] = i;
  }

}

考虑到后缀数组的特殊性  我们换一种比较方式

一首先考虑到比较方便我们把所有的字母都减去 a-1  这里我只考虑所有字母都是小写字母的方式

加入字符串是  aabaaaab

技术分享

 

下面将相邻俩个合并为一个整数 

技术分享

这样下面使用基数排序对这个合并后的整数进行排序 为什么使用基数排序 因为它的位数固定 也许你会问那

字母 z 减去‘a’- 1 不是大于10了吗 那不是3位数了吗   不是这样的  把 z 减去‘a’- 1 =26 看做是一个数 而不是二十六 

将相当于16进制 一样15不是看做两位数 而是用F来表示  当然你高兴 完全可以吧26写作Z以后 Z就是26

下面我讲解一下  这个很重要 为什么要两两合并为一个数

首先求所有后缀数组最后组成为下图

技术分享

那么每一个后缀之间都是有重复的 第1个后缀的前两个就是第0个后缀的第一到第三个字母

那么一次类推  也就是说我按下图分为两两一组  

技术分享

将上图的两两一组一个整数按照基数排序的结果为

技术分享

解释一下 第一个11 排第一名   第二个12 排第二名

那么你有没有发现第0个后缀到第7个后缀的前两个字母的比较已经出来了 因为第一个11 就是第0个后缀的前两个字母 第二个12 就是第1个后缀的前两个字母

好了 现在我们已经比较所有后缀的前两个字母  下面我开始比较后面 那么我怎么比较前两个字母后面的字符串呢  因为刚才我已经把所有的两两字母的大小已经比较出来了  我现在可以利用下面的结果再比较  看图

技术分享

那么现在对1121    1211     2111   1111  1111    1120  1100  2000进行排序  分成两组 前两个字母一组后两个字母一组 比如 1121这四个数字  11   与 21 两份来基数排序 

技术分享

等等  你有没有发现 我们上面的排序后的排名 跟第一关键字与第二关键字 有关系 也就是说

排名的大小就是第二关键字排名的  为什么 因为排序后的排名就是 第二关键字的排序结果

那么与第一关键字有什么关系 ? 有没有发现 就是把第一关键字的11去掉 然后再加一个00

那么我可以这样  也就是00直接替换11的位置  其他位置的排名不变 (这个要好好想想)

举个生动的例子  现在有很多人在排队  高矮不等 ,一开始是乱序的  现在保安要求 按从矮到高排列

排好序之后  大家都有了自己的位置  现在保安走开了 队伍又回到一开始的状态  并且原来站在最开始的人(乱序是的站在最开始的人)走了  来了一个小矮人 肯定是最矮的  保安回来 要求再次排队  那么小矮人肯定站在最前面   下面保安喊道 上次排序排第一的人接上  如果走的那个人是第一  那么就继续后面  如果不是上次排名第一的人就站上来    然后保安继续叫  一直到上次排名最后的一个 

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

浅谈后缀数组

[Go] 通过 17 个简短代码片段,切底弄懂 channel 基础

后缀数组记录

后缀自动机 模板题

后缀数组模板及解释

POJ - 2406 Power Strings (后缀数组DC3版)