彻底弄懂后缀数组

 

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

比如 字符串 abcdef  那么 abcdef    bcdef    cdef     def       ef       f 就叫做后缀  也就是从最后一个字母之前的一个字母开始一直到最后一个字母(所以所 bcd不是后缀 因为没有到最后一位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]存放各个后缀的优先级)

  名次数组 Rank[i] 保存的是 以下标 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;
  }

}

  

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

为什么称它特殊  因为一个字符串所有的后缀之间是有关系的

比如以字符串 abcdef  来举例

后缀bcdef 与 cdef  就有比较强的关系  后一个是前一个的组成部分  准确来说是后半组成部分

那么怎么利用他们这种关系 请继续看

一首先考虑到比较方便我们把所有的字母都减去 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 就是第1个后缀的前两个字母 第二个12 就是第2个后缀的前两个字母

什么意思 看图

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

下面开始再次拼接 如图 最后这号拼成八位数  也就是正好字符串的长度 这时候可以使用基数排序来比较  但是假如字符串10000个呢  那么有10000个后缀  每个后缀的长度是10000 意味着最后拼接成的数也是有10000位   10000*10000我们需要开辟这么大数据这是不可行的   那么我们能不能将每次拼接的大数缩小呢?

先看图

首先后缀数组最终要获得的是后缀的排名 那么到底是1112  还是 11   是1221 还是24 无所谓

我只要把他们保持合适的大小  就比如说 小明考了100分  小红考了89分  小刚考了55分

那么我现在把小刚设为0分  小红设为1分 小明设为2分 那么对他们最后的排名有影响吗  没有

小明还是第一名  就是这个道理  这样我们可以最大程度减小存储的开销

所以我只要每次对合并的数据进行按照从小到大排个序  用序号替换它  然后再次按照之前的步骤再次合并  再次排序替换  (什么时候结束)当全部的字符串都参与了比较就停止了

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

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

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

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

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

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

上面这个故事就对应于下面的代码

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;

时间: 2024-08-05 07:08:16

彻底弄懂后缀数组的相关文章

完全弄懂后缀数组

什么叫后缀数组  首先要知道什么叫后缀 比如 字符串 abcdef  那么 abcdef bcdef cdef def ef f 就叫做后缀  也就是从最后一个字母之前的一个字母开始一直到最后一个字母  所构成的字符串就叫做后缀 至于后缀数组能干什么?我在这就不介绍了  我想你既然知道后缀数组就一定知道他的用处 但是自己之前读过很多后缀数组的文章  短短二三十代码  却没有找到一篇博客从头到尾讲解的 自己断断续续一个月终于算是对倍增算法(就是一个名字  不必纠结什么叫倍增算法)的有个比较深入理解

10分钟弄懂javascript数组

建议阅读时间 : 10分钟 主要内容:javascript数组的基本概念.属性.方法 新建数组: var arr01 = ["a","b","c","d"]; var arr02 = new Array("a","b","c","d"); 上面的两个方法都可以创建数组 ["a","b","c&quo

后缀数组模板及解释

以前做过后缀数组,直接用模板,最近打算重新认真的学一遍.感觉学一个东西一定要弄懂了,不然到最后还是要重学. int wa[MAXN],wb[MAXN],wv[MAXN],Ws[MAXN]; void da(int *r,int *sa,int n,int m){//n表示字符串长度 + 1,包括添加的那个0,m表示取值的范围 //把单个字符进行基数排序 int *x = wa,*y = wb; for(int i = 0; i < m; i++)Ws[i] = 0; for(int i = 0;

后缀数组 &amp; 题目

后缀数组被称为字符串处理神器,要解决字符串问题,一定要掌握它.(我这里的下标全部都是从1开始) 首先后缀数组要处理出两个数组,一个是sa[],sa[i]表示排名第i为的后缀的起始位置是什么,rank[i]表示第i个字符为起始点的后缀,它的排名是什么.可以知道sa[rank[i]] = i; rank[sa[i]] = i; 由于每个后缀各不相同,至起码长度不同,所以每个后缀是不可能相等的. 解除一个值,就能在O(n)时间内得到另外一个. 定义:suffix(i)表示从[i, lenstr]这个后

kuangbin带你飞 后缀数组 题解

2份模板 DC3 . 空间复杂度O3N 时间复杂度On #define F(x) ((x) / 3 + ((x) % 3 == 1 ? 0 : tb)) #define G(x) ((x) < tb ? (x) * 3 + 1 :((x) - tb) * 3 + 2) const int MAXN = 300010; const int MAXM = 100010; char input[MAXM]; int wa[MAXN],wb[MAXN],ws[MAXN],wv[MAXN],wsd[MAX

POJ 3415 Common Substrings(长度不小于k 的公共子串的个数--后缀数组+单调栈优化)

题意:给定两个字符串A 和B,求长度不小于k 的公共子串的个数(可以相同). 样例1: A="xx",B="xx",k=1,长度不小于k 的公共子串的个数是5. 样例2: A ="aababaa",B ="abaabaa",k=2,长度不小于k 的公共子串的个数是22. 思路: 如果i后缀与j后缀的LCP长度为L, 在L不小于K的情况下, 它对答案的贡献为L - K + 1. 于是我们可以将两个串连起来, 中间加个奇葩的分隔符

后缀数组Da模板+注释 以及 dc3模板

后缀数组Da模板: 1 /* 2 后缀数组倍增法Da板子 3 */ 4 #include <cstdlib> 5 #include <cstring> 6 #include <cstdio> 7 #include <algorithm> 8 using namespace std; 9 const int N = 200000+9; 10 int c[N]; 11 int rank[N], height[N]; 12 int sa[N],s[N],n; 13

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

题意:求最小循环节循环的次数. 题解:这个题其实可以直接用kmp去求最小循环节,然后在用总长度除以循环节.但是因为在练后缀数组,所以写的后缀数组版本.用倍增法会超时!!所以改用DC3法.对后缀数组还不是很理解,找了很多博客也没看懂到底有些数组到底记录的是啥,但他的实现过程很好理解,等我弄懂了再来给博客加注释吧. 先求出sa数组,height数组,rank数组(因为和c++库中某个东西重了所以写成rnk数组),数组一定要开3倍.接下来从小到大枚举循环节长度 i,如果长度i的子串刚好是重复了len/

HDU_6194 后缀数组+RMQ

好绝望的..想了五个多小时,最后还是没A...赛后看了下后缀数组瞬间就有了思路...不过因为太菜,想了将近两个小时才吧这个题干掉. 首先,应当认为,后缀数组的定义是,某字符串S的所有后缀按照字典序有小到大的顺序排列(使用下标表示后缀).因为具体过程没太看懂,但是参见刘汝佳蓝书<算法竞赛黑暗圣典>可以得到一个聪明的NLOGN的神器算法.不过这个不太重要. 之后还可以通过他在LCP问题中提到的RANK,height数组相关算法,处理出来height数组,之后其他的可以扔掉. <黑暗圣典>