【后缀数组】【真难懂啊】

基本上一搜后缀数组网上的模板都是《后缀数组——处理字符串的有力工具》这一篇的注释,O(nlogn)的复杂度确实很强大,但对于初次接触(比如窝)的人来说理解起来也着实有些困难(比如窝就活活好了两天的光阴。。),看了那么多材料感觉《挑战程序设计》的后缀数组解释理解起来会相对容易很多,然而它的复杂度是O(nlog2n)的,主要区别是对子串排序的时候前者用了基数排序--O(n),而后者用了快排--O(nlogn),这就导致了最终的复杂度后者比前者多了一个O(logn)。

先整理下《挑战》上的程序,晚些再将O(nlogn)整理下来;

先附清爽版求SA(Suffix_Array)模板,主要思想当然仍是倍增法;

 1 /*0(nlog(n)^2)*/
 2 #include <iostream>
 3 #include <cstring>
 4 #include <cstddef>
 5 #include <cstdio>
 6 #include <string>
 7 #include <algorithm>
 8 using namespace std;
 9 const int MAXN = 10001;
10 int n,k;
11 int rank[MAXN+1],tmp[MAXN+1];
12
13 bool comp_sa(int i, int j)
14 {
15     if(rank[i] != rank[j])
16         return rank[i] < rank[j];
17     int ri = i+k <= n? rank[i+k] : -1;
18     int rj = j+k <= n? rank[j+k] : -1;
19     return ri < rj;
20 }
21
22 void calc_sa(string &S, int *sa) //计算字符串S的后缀数组
23 {
24     n = S.size();
25     //初始长度为1
26     for(int i = 0; i <= n; i++)
27     {
28         sa[i] = i;
29         rank[i] = i < n ? S[i] : -1;
30     }
31
32     for( k = 1; k <= n; k *= 2)
33     {
34         sort(sa,sa+n+1,comp_sa); //双关键字快排
35
36         //先在tmp中临时存储新计算的rank,再转存回rank中
37         tmp[sa[0]] = 0;
38         for(int i = 1; i <= n; i++)
39         {
40             tmp[sa[i]] = tmp[sa[i-1]] + (comp_sa(sa[i-1],sa[i]) ? 1: 0);
41         }
42         for(int i = 0; i <= n; i++)
43         {
44             rank[i] = tmp[i];
45         }
46     }
47 }
48
49 int main()
50 {
51     string S = "abracadabra";
52     int *sa = new int[S.size()+1];
53      SuffixArrayMatch(S,sa,T);
54      delete [] sa;
55      sa = NULL;
56 }

当然初次接触的人看这代码必然不知这是什么鬼,建议可以多找几篇blog的代码注释(有很多大神的注释很细致很不错),加上手动模拟理解一下,多看几遍肯定会开窍的~;

定义(理解!):

后缀数组Suffix_Array:SA[]

  将某个字符串的所有后缀按字典序排序后得到的数组;

  SA[i] = k即表示排序后第i小的子串为S[k ... n](为好理解暂用字符串下标1~n);

  而最终我们要达到的SA数组状态如下例所示(摘自罗神的PPT):

名次数组:Rank[]

  保存后缀S[i ... n]在排序中的名次;

  rank[i] = k即表示子串S[i ... n]在所有后缀的字典序排序中为第k小;

通过图1可以看出SA[1] = 4, Rank[4] = 1; 即SA数组与Rank数组为互逆关系

后缀数组的计算

算法的基本思想 -- 倍增

什么叫倍增?

  首先计算每个位置开始的长度为1的子串的顺序,利用该结果计算长度为2的子串的顺序,再利用长度为2的子串的顺序结果计算长度为4的子串的顺序 ...... 不断倍增,知道长度大于等于原字符串长度,就得到了后缀数组;

  要计算长度为2的子串的顺序,只要排序两个字符组成的数对即可。

  比如原字符串 aaba (下面各字母的下标代表该字母在原字符串的位置)

  a1=a2=a4 < b3,那么a1a2一定小于a2b3

  要求长度为2k的子串的顺序,只要知道长度为k的子串的顺序即可。

  比如原字符串 aabac

  a1a2 < a2b3 < a4c5< b3a4,那么 a1a2b3a4 一定小于 a2b3a4c5

  记rankk(i)为S[i, k](从i开始的长度为k的子串)在所有排好序的长度为k的子串中是第几小的;

  要计算长度为2k的子串的顺序,就只要对两个rank组成的数对进行排序即可。通过rankk(i)与rankk(i+k)的数对和rankk(j)与rankk(j+k)的数对比较(双元素比较)来代替对S[i, 2k]和S[j, 2k]的直接比较。因为比较rankk(i)与rankk(j)就相当于比较S[i, k]与S[j, k],比较rankk(i+k)与rankk(j+k)就相当于比较S[i+k, k]与S[j+k, k]。

举个例子: abracadabra

初始化:SA[i] = i;

        rank[i] = S[i]; //初始的时为对字符串中的单个字符排序,故可以直接将rank初始为字符的ASCII码,注意此时的rank并非实际意义上的排序,仅仅是相对排序,即S[i]>S[j]则rank[i] > rank[j]而已;

    还有需要注意的一点是此处有一个处理字符串的小技巧是将SA[n] 定义为 -1;这样以后的rank值就可以从1开始排了。

k = 0,初始化,得到S[i, 1]的排序;

  sa[0] : 0         rank[0] : 97
  sa[1] : 1         rank[1] : 98
  sa[2] : 2         rank[2] : 114
  sa[3] : 3         rank[3] : 97
  sa[4] : 4         rank[4] : 99
  sa[5] : 5         rank[5] : 97
  sa[6] : 6         rank[6] : 100
  sa[7] : 7         rank[7] : 97
  sa[8] : 8         rank[8] : 98
  sa[9] : 9         rank[9] : 114
  sa[10] : 10      rank[10] : 97
  sa[11] : 11  rank[11] : -1

k = 1;得到S[i, 2]的排序;

  sa[0] : 11               rank[11] : 0
  sa[1] : 10       a      rank[10] : 1
  sa[2] : 0         ab     rank[0] : 2
  sa[3] : 7         ab     rank[7] : 2
  sa[4] : 3         ac     rank[3] : 3
  sa[5] : 5         ad     rank[5] : 4
  sa[6] : 1         br     rank[1] : 5
  sa[7] : 8         br     rank[8] : 5
  sa[8] : 4         ca     rank[4] : 6
  sa[9] : 6         da     rank[6] : 7
  sa[10] : 2        ra     rank[2] : 8
  sa[11] : 9        ra     rank[9] : 8

k = 2;得到S[i, 4]的排序;

  sa[0] : 11                rank[11] : 0
  sa[1] : 10        a        rank[10] : 1
  sa[2] : 0         abra      rank[0] : 2
  sa[3] : 7         abra      rank[7] : 2
  sa[4] : 3         acad      rank[3] : 3
  sa[5] : 5         adab      rank[5] : 4
  sa[6] : 8         bra        rank[8] : 5
  sa[7] : 1         brac      rank[1] : 6
  sa[8] : 4         cada      rank[4] : 7
  sa[9] : 6         dabr      rank[6] : 8
  sa[10] : 9        ra         rank[9] : 9
  sa[11] : 2        raca      rank[2] : 10

k = 4;得到S[i, 8]的排序;

  sa[0] : 11                  rank[11] : 0
  sa[1] : 10        a          rank[10] : 1
  sa[2] : 7         abra        rank[7] : 2
  sa[3] : 0         abracada     rank[0] : 3
  sa[4] : 3         acadabra     rank[3] : 4
  sa[5] : 5         adabra        rank[5] : 5
  sa[6] : 8         bra          rank[8] : 6
  sa[7] : 1         bracadab     rank[1] : 7
  sa[8] : 4         cadabra       rank[4] : 8
  sa[9] : 6         dabra          rank[6] : 9
  sa[10] : 9         ra              rank[9] : 10
  sa[11] : 2         racadabr      rank[2] : 11

k = 8;得到S[i, n]的排序;

  sa[0] : 11                  rank[11] : 0
  sa[1] : 10        a          rank[10] : 1
  sa[2] : 7         abra        rank[7] : 2
  sa[3] : 0         abracadabra    rank[0] : 3
  sa[4] : 3         acadabra       rank[3] : 4
  sa[5] : 5         adabra        rank[5] : 5
  sa[6] : 8         bra         rank[8] : 6
  sa[7] : 1         bracadabra     rank[1] : 7
  sa[8] : 4         cadabra      rank[4] : 8
  sa[9] : 6         dabra       rank[6] : 9
  sa[10] : 9        ra          rank[9] : 10
  sa[11] : 2        racadabra      rank[2] : 11

这样就得到了SA数组;

对于字符串的排序采用双关键字快排,复杂度为O(nlogn);每次计算后缀S[i ... n] 的 rank值时,先与该后缀排序后的前一个后缀比较,如果相等则rank值相同,否则rank值加1;

1 //先在tmp中临时存储新计算的rank,再转存回rank中
2 tmp[sa[0]] = 0;
3 for(int i = 1; i <= n; i++)
4     tmp[sa[i]] = tmp[sa[i-1]] + (comp_sa(sa[i-1],sa[i]) ? 1: 0);
5
6 for(int i = 0; i <= n; i++)
7     rank[i] = tmp[i];

晚些整理O(nlogn)算法

时间: 2024-10-09 00:43:59

【后缀数组】【真难懂啊】的相关文章

后缀数组 DC3构造法 —— 详解

学习了后缀数组,顺便把DC3算法也看了一下,传说中可以O(n)复杂度求出文本串的height,先比较一下倍增算法和DC3算法好辣. DC3 倍增法 时间复杂度 O(n)(但是常数很大)   O(nlogn)(常数较小) 空间复杂度   O(n)    O(n) 编程复杂度    较高   较低 由于在时间复杂度上DC3的常数比较大,再加上编程复杂度比较高,所以在解决问题的时候并不是最优选择.但是学到了后缀数组还是补充一下的好点. DC3算法的实现: 1:先把文本串的后缀串分成两部分,第一部分是后

后缀数组构造

读了罗穗的论文,终于知道后缀数组怎么构造了,还反复打了五遍,打得很痛苦才最终理解. 终于体会到XY的痛苦了.实在是一篇OI生涯中最难懂的代码orz看来两天.一下是经过稍微加长的稍微易理解的代码(其实差不多好吧). 具体注释看 网址 1 #include<cstdio> 2 #include<string.h> 3 #include<iostream> 4 using namespace std; 5 6 struct suffix_array 7 { 8 const l

后缀数组详解+模板

后缀数组 注 SA[] 第几名是谁 后缀数组:后缀数组 SA 是一个一维数组, 它保存 1..n 的某个排列 SA[1] ,SA[2],……,SA[n],并且保证 Suffix(SA[i]) < Suffix(SA[i+1]),1≤i<n .也就是将 S 的 n 个后缀从小到大进行排序之后把排好序的后缀的开头位置顺次放入 SA 中. Rank[] 谁是第几名名次数组:名次数组 Rank[i]保存的是 Suffix(i)在所有后缀中从小到大排列的“名次 ” . r[]:原始数据j当前字符串的长度

[知识点]后缀数组

// 本文部分内容参照刘汝佳<算法竞赛入门经典训练指南>,特此说明. 1.前言 趁着这几天上午,把后缀数组大致看完了.这个东西本身的概念可能没太大理解问题,但是它所延伸出来的知识很复杂,很多,还有它的两个兄弟——后缀树,后缀自动机,编起来都不是盖的. 2.概念 前面曾经提到过Aho-Corasick自动机(http://www.cnblogs.com/jinkun113/p/4682853.html),讲得有点简略...它用以解决多模板匹配问题.但是前提是事先知道所有的模板,在实际应用中,我们

HDU 5008西安网络赛B题:后缀数组求第k小子串

思路:尼玛,这题搞了一天了,比赛的时候用了n^2的方法绝对T了,然后今天看别人代码看了一天才知道.后面感觉也挺容易的,就是没想到,之前做过SPOJ 694 705求过不同子串了,知道怎么求不同子串个数了,但是比赛的时候这个技巧竟然抛在脑后了,然后就不会了. 但是今天自己用了自己的两个后缀数组的模板(倍增和DC3)的都WA了,搞得自己真想跳楼去了!! 到现在都不知道到底是哪里错了,处理的方法和标准做法都一样,但是就是WA,然后用了别人的模板,再用自己的处理方法就过了,怀疑自己的两个模板是不是哪里错

后缀数组 TYVJ P1860 后缀数组

/*P1860 后缀数组时间: 1000ms / 空间: 131072KiB / Java类名: Main描述 我们定义一个字符串的后缀suffix(i)表示从s[i]到s[length(s)]这段子串.后缀数组(Suffix array)SA[i]中存放着一个排列,满足suffix(sa[i])<suffix(sa[i+1]) 按照字典序方式比较定义height[i]表示suffix(sa[i])与suffix(sa[i-1])之间的最长公共前缀长度,其中height[1]=0你的任务就是求出

BZOJ 2754 SCOI 2012 喵星球上的点名 后缀数组

题目大意:在喵星球上有一些喵~,每个喵都有一个姓和一个名字.点名的时候如果一个喵中姓或者名中有这个串的话他就会喵.问每次点名有几个喵喵了,和每个喵喵了几次. 思路:好萌的题喵~ AC自动机构造fail树是可以做的,但是和SA乱搞的时间差不多,我就是SA乱搞的w 把所有的串(姓名,询问)用$连接成一个串,然后做后缀数组,height数组.过程中记录一下每一个后缀数属于哪个喵,还有询问在串中的起始位置.在处理询问的时候,可以同过sa,rank数组快速的访问height数组,对于每一个询问向两边拓展,

后缀数组模板(理解)

字符串的处理真可谓是博大精深,后缀数组这种数据结构我花了两天时间才明白了其构造的过程.主要是代码不好理解. 数据结构: 1.sa数组,就是后缀数组,按照字典序排列,其意义为:sa[i]=k,排第i名的子串是从k位开始的. 2.rank名次数组,其意义为:rank[i]=k,以i为起点的子串排名为k. 很容易看出来两者可以相互转化. 求这两个数组的过程是基于基数排序,计数排序的方法. 下面是一个大牛的注释版.其中我在补充点: 1.对于求y[]这个数组,因为已经知道上次长为j的子串比较结果,那么这次

后缀数组-倍增法

额,写的有点混乱,改天整理一下 链接:https://hihocoder.com/problemset/problem/1403 1 //字符串从1开始 2 //rank[i]为字符串中i位置起的后缀排序后的顺序 3 //sa[i]为排序后的第i位置对应的后缀在字符串中的起始位置 4 //sa[rank[i]]=i此公式在最终结果才成立 5 //height[i]为排序数组中i位置与i-1位置的最长公共前缀的长度 6 //H[i]为字符串i位置起的后缀与其在排序后前面相邻位置处的后缀的最大公共前