转自:http://blog.csdn.net/yxuanwkeith/article/details/50636898 五分钟搞懂后缀数组!后缀数组解析以及应用(附详解代码) 作者:YxuanwKeith

为什么学后缀数组

后缀数组是一个比较强大的处理字符串算法,是有关字符串的基础算法,所以必须掌握。 
学会后缀自动机(SAM)就不用学后缀数组(SA)了?,虽然SAM看起来更为强大和全面,但是有些SAM解决不了的问题能被SA解决,只掌握SAM是远远不够的。 
……

有什么SAM做不了的例子? 
比如果求一个串后缀的lcp方面的应用,这是SA可以很方便的用rmq来维护,但是SAM还要求lca,比较麻烦,还有就是字符集比较大的时候SA也有优势。

现在这里放道题,看完这个blog可能就会做了!: 
你可想想这道题:你有一个01串S,然后定义一个前缀最右边的位置就是这个前缀的结束位置。现在有q多个询问,每个询问结束位置在l~r中不同前缀的最长公共后缀是多长? 
|S|,q≤100000 
时限4s

而下面是我对后缀数组的一些理解

构造后缀数组——SA

先定义一些变量的含义

Str :需要处理的字符串(长度为Len) 
Suffix[i] :Str下标为i ~ Len的连续子串(即后缀) 
Rank[i] : Suffix[i]在所有后缀中的排名 
SA[i] : 满足Suffix[SA[1]] < Suffix[SA[2]] …… < Suffix[SA[Len]],即排名为i的后缀为Suffix[SA[i]] (与Rank是互逆运算) 
好,来形象的理解一下 
 
后缀数组指的就是这个SA[i],有了它,我们就可以实现一些很强大的功能(如不相同子串个数、连续重复子串等)。如何快速的到它,便成为了这个算法的关键。而SARank是互逆的,只要求出任意一个,另一个就可以O(Len)得到。 
现在比较主流的算法有两种,倍增DC3,在这里,就主要讲一下稍微慢一些,但比较好实现以及理解的倍增算法(虽说慢,但也是O(Len logLen))的。

进入正题——倍增算法

倍增算法的主要思想 :对于一个后缀Suffix[i],如果想直接得到Rank比较困难,但是我们可以对每个字符开始的长度为2k的字符串求出排名,k从0开始每次递增1(每递增1就成为一轮),当2k大于Len时,所得到的序列就是Rank,而SA也就知道了O(logLen)枚举k 
这样做有什么好处呢? 
设每一轮得到的序列为rank(注意r小写,最终后缀排名Rank大写)。有一个很美妙的性质就出现了!第k轮的rank可由第k - 1轮的rank快速得来! 
为什么呢?为了方便描述,设SubStr(i, len)为从第i个字符开始,长度为len的字符串我们可以把第k轮SubStr(i, 2k)看成是一个由SubStr(i, 2k−1)SubStr(i + 2k−1, 2k−1)拼起来的东西。类似rmq算法,这两个长度而2k−1的字符串是上一轮遇到过的!当然上一轮的rank也知道!那么吧每个这一轮的字符串都转化为这种形式,并且大家都知道字符串的比较是从左往右,左边和右边的大小我们可以用上一轮的rank表示,那么……这不就是一些两位数(也可以视为第一关键字和第二关键字)比较大小吗!再把这些两位数重新排名就是这一轮的rank。 
我们用下面这张经典的图理解一下: 
 
相信只要理解字符串的比较法则(跟实数差不多),理解起来并不难。#还有一个细节就是怎么把这些两位数排序?这种位数少的数进行排序毫无疑问的要用一个复杂度为长度*排序数的个数的优美算法——基数排序(对于两位数的数复杂度就是O(Len)的)。 
基数排序原理 : 把数字依次按照由低位到高位依次排序,排序时只看当前位。对于每一位排序时,因为上一位已经是有序的,所以这一位相等或符合大小条件时就不用交换位置,如果不符合大小条件就交换,实现可以用”桶”来做。(叙说起来比较奇怪,看完下面的代码应该更好理解,也可以上网查有关资料) 
好了SARank(大写R)到此为止就处理好了。(下面有详解代码!)。但我们发现,只有这两样东西好像没什么用,为了处理重复子串之类的问题,我们就要引入一个表示最长公共前缀的新助手Height数组!

构造最长公共前缀——Height

同样先是定义一些变量

Heigth[i] : 表示Suffix[SA[i]]和Suffix[SA[i - 1]]的最长公共前缀,也就是排名相邻的两个后缀的最长公共前缀 
H[i] : 等于Height[Rank[i]],也就是后缀Suffix[i]和它前一名的后缀的最长公共前缀 
而两个排名不相邻的最长公共前缀定义为排名在它们之间的Height的最小值。 
跟上面一样,先形像的理解一下: 

高效地得到Height数组

如果一个一个数按SA中的顺序比较的话复杂度是O(N2)级别的,想要快速的得到Height就需要用到一个关于H数组的性质。 
H[i] ≥ H[i - 1] - 1! 
如果上面这个性质是对的,那我们可以按照H[1]、H[2]……H[Len]的顺序进行计算,那么复杂度就降为O(N)了! 
让我们尝试一下证明这个性质 : 设Suffix[k]是排在Suffix[i - 1]前一名的后缀,则它们的最长公共前缀是H[i - 1]。都去掉第一个字符,就变成Suffix[k + 1]Suffix[i]如果H[i - 1] = 0或1,那么H[i] ≥ 0显然成立。否则H[i] ≥ H[i - 1] - 1(去掉了原来的第一个,其他前缀一样相等),所以Suffix[i]和在它前一名的后缀的最长公共前缀至少是H[i - 1] - 1。 
仔细想想还是比较好理解的。H求出来,那Height就相应的求出来了,这样结合SA,Rank和Height我们就可以做很多关于字符串的题了!

 /*
     Problem: JZOJ1598(询问一个字符串中有多少至少出现两次的子串)
     Content: SA's Code and Explanation
     Author : YxuanwKeith
 */

 #include <cstdio>
 #include <cstring>
 #include <algorithm>

 using namespace std;

 ;

 char ch[MAXN], All[MAXN];
 int SA[MAXN], rank[MAXN], Height[MAXN], tax[MAXN], tp[MAXN], a[MAXN], n, m;
 char str[MAXN];
 //rank[i] 第i个后缀的排名; SA[i] 排名为i的后缀位置; Height[i] 排名为i的后缀与排名为(i-1)的后缀的LCP
 //tax[i] 计数排序辅助数组; tp[i] rank的辅助数组(计数排序中的第二关键字),与SA意义一样!
 //a为原串
 void RSort() {
     //rank第一关键字,tp第二关键字。
     ; i <= m; i ++) tax[i] = ;//计数初始化
     ; i <= n; i ++) tax[rank[tp[i]]] ++;
     ; i <= m; i ++) tax[i] += tax[i-];//前缀和求出排名
     ; i --) SA[tax[rank[tp[i]]] --] = tp[i]; //确保满足第一关键字的同时,再满足第二关键字的要求
 } //计数排序,把新的二元组排序。

 int cmp(int *f, int x, int y, int w) { return f[x] == f[y] && f[x + w] == f[y + w]; }
 //通过二元组两个下标的比较,确定两个子串是否相同

 void Suffix() {
     //SA
     ; i <= n; i ++) rank[i] = a[i], tp[i] = i;
     m =  ,RSort(); //一开始是以单个字符为单位,所以(m = 127)

     , p = , i; p < n; w += w, m = p) { //把子串长度翻倍,更新rank

         //w 当前一个子串的长度; m 当前离散后的排名种类数
         //当前的tp(第二关键字)可直接由上一次的SA的得到
         , i = n - w + ; i <= n; i ++) tp[++ p] = i; //长度越界,第二关键字为0
         ; i <= n; i ++) if (SA[i] > w) tp[++ p] = SA[i] - w;

         //更新SA值,并用tp暂时存下上一轮的rank(用于cmp比较)
         RSort(), swap(rank, tp), rank[SA[]] = p = ;

         //用已经完成的SA来更新与它互逆的rank,并离散rank
         ; i <= n; i ++) rank[SA[i]] = cmp(tp, SA[i], SA[i - ], w) ? p : ++ p;
     }
     //离散:把相等的字符串的rank设为相同。
     //LCP
     ;
     ; i <= n; Height[rank[i ++]] = k)
          : k, j = SA[rank[i] - ]; a[i + k] == a[j + k]; ++ k);
     //这个知道原理后就比较好理解程序
 }

 void Init() {
     scanf("%s", str);
     n = strlen(str);
     ; i < n; i ++) a[i + ] = str[i];
 }

 int main() {
     Init();
     Suffix();

     ];
     ; i <= n; i ++) ans += max(Height[i] - Height[i - ], );
     printf("%d\n", ans);
 }

神奇的代码

4个比较基础的应用

Q1:一个串中两个子串的最大公共前缀是多少? 
A1:这不就是Height吗?用rmq预处理,再O(1)查询。 
 
Q2:一个串中可重叠的重复最长子串是多长? 
A2:就是求任意两个后缀的最长公共前缀,而任意两个后缀的最长公共前缀都是Height 数组里某一段的最小值,那最长的就是Height中的最大值。 
 
Q3:一个串中不可重叠的重复最长子串是多长? 
A3:先二分答案转化成判别式的问题比较好处理。假设当前需要判别长度为k是否符合要求,只需把排序后的后缀分成若干组,其中每组的后缀之间的Height 值都不小于k,再判断其中有没有不重复的后缀,具体就是看最大的SA值和最小的SA值相差超不超过k,有一组超过的话k就是合法答案。 
 
A4:一个字符串不相等的子串的个数是多少? 
Q4:每个子串一定是某个后缀的前缀,那么原问题等价于求所有后缀之间的不相同的前缀的个数。而且可以发现每一个后缀Suffix[SA[i]]的贡献是Len - SA[i] + 1,但是有子串算重复,重复的就是Heigh[i]个与前面相同的前缀,那么减去就可以了。最后,一个后缀Suffix[SA[i]]的贡献就是Len - SA[k] + 1 - Height[k]。 
对于后缀数组更多的应用这里就不详细阐述,经过思考后每个人都会发现它的一些不同的用途,它的功能也许比你想象中的更强大!

最开始的那道题

先搬下来。。。

你可想想这道题:你有一个01串S,然后定义一个前缀最右边的位置就是这个前缀的结束位置。现在有很多个询问,每q个询问结束位置在l~r中不同前缀的最长公共后缀是多长? 
|S|,q≤100000 
时限4s

简单思路:首先可以把字符串反过来就是求后缀的最长公共前缀了,可以用SA求出height数组,然后用rmq预处理之后就是求两个位置间的最小值。然后对于一个区间,显然只有在SA数组中相邻的两个串可以贡献答案。 
对于区间询问的问题可以用莫队处理,然后考虑加入一个后缀应该怎么处理,我们可以维护一个按SA数组排序的链表。假设我们先把所有位置的SA全部加入,然后按顺序删除,重新按顺序加入时就可以O(1)完成修改。那么按照这个思路我们可以用固定左端点的并查集,做到只加入,不删除,然后用O(nn√+nlogn)的复杂度完成这道题。

*可能后面的处理方式比较麻烦,如果直接用splay维护区间中的后缀的话可以做到O(nn√logn),这个方法就比较直观,而SAM在个问题上还是有点无力的。这题只是为了说明SA相比于SAM还是有他的独到之处,特别是在处理后缀的lcp之类的问题上。

结束

以上就是我对后缀数组的理解 ——YxuanwKeith

 

简要总结

后缀数据充分利用了先前的信息,使得效率有可观性地提高。

suddix[i]表示后缀开始位置为i的后缀字符串

rank[i]表示第i个后缀的排名

sa[i]表示排名第i的后缀位置

height[i]表示排名为i和i-1的后缀字符串的最长公共前缀长度

以下是个人关于几个应用的理解

1.求字符串中可重叠的最长公共子串

根据height定义很显然这个就是height中的最大值了。

2.求字符串中不可重叠的最长公共子串

这个我们需要二分答案再验证,我们要二分可能的公共子串长度k,然后按sa的顺序对height进行分组,使组内的height值都不小于k,然后对于某个组内我们只要考察该组内sa的最大值和最小值的差是否大于等于k(实际上就是这两个后缀的开头是否相差k从而避免重叠),有则k成立。

3.求字符串中可重叠K次的最长公共子串

这个我们跟2差不多,二分公共长度k分组,然后我们考察每个组内的后缀个数是否大于等于K,有则K成立。

4.求字符串中不相同的子串个数

每个子串必定是某个后缀的前缀,那问题就是求所有后缀中不相同的前缀的个数,我们从顺序sa[1],sa[2],sa[3],不难发现每加入一个suffix[sa[i]],它有n-sa[i]+1个前缀(就是这个后缀的长度),其中有height[i]是和前面的字符串相同(最长公共前缀嘛),所以这个字符串会贡献出n-sa[i]+1-height[i]不同的子串,累加后就可以了。

5.求字符串中最长的回文子串

所谓回文就是一个字符串满足中心对称,某个字符为对称中心,从这个字符向左和向右对应位置的字母都相等,如,我们设这个中心对称的字符为a[i],则我们就要判断 a[i-k]与a[i+k]是否相等,我们可以把整个字符串的字符串),其中加个特殊符号,这样我们可以简化判断,只用判断这新的字符串的某两个字符串的最长公共前缀

6.求字符串中连续的重复子串

已知一个字符串L是由某个字符串S重复R次得到的求最大值。

我们假设S的长度为k,首先L%k=0,然后判断suffix[1],和suffix[k+1]的最长公共子串是否为n-k。因此在查找最长公共子串的时候就是求height[rank[k+1]]到height[rank[1]]之间的最小值。因此我们的做法就是求height数组中每一个数到height[rank[1]]之间的数的最小值k,R=L的长度/K

7.求字符串中重复次数最多的连续重复子串

8.求两个字符串的最长公共子串

将这两个字符串连接起来,其中用一个特殊符号分开,然后再求出不在同一个字符串中的最大的height值即可。

9.求长度不小于K的最长公共子串

将两个字符串A、B连接起来,其中用一个特殊符号分开,然后用k对height数组分组,再统计每组的最长公共前缀和。每遇到一个B子串,就统计与前面A子串产生多少个长度不小于K的公共子串,这里A需要用栈来维护。然后对A也一样的处理。

10.求n个字符串的最长公共子串

这个用KMP可以处理,也可以将这n个字符串连成一个字符串,然后用不同的特殊符号分开,二分长度k对height数组进行分组判定是否该组中所有字符串的子串都出现在里面即可。

【转载&总结】后缀数组及广泛应用的更多相关文章

  1. poj3581Sequence(后缀数组)

    转载请注明出处: http://www.cnblogs.com/fraud/          ——by fraud Sequence Time Limit: 5000MS   Memory Limi ...

  2. poj 2774 最长公共子--弦hash或后缀数组或后缀自己主动机

    http://poj.org/problem?id=2774 我想看看这里的后缀数组:http://blog.csdn.net/u011026968/article/details/22801015 ...

  3. POJ 2217 Secretary (后缀数组)

    标题效果: 计算两个公共串串最长的字符串的长度. IDEAS: 这两个组合的字符串. 然后直接确定运行后缀数组height 然后,你可以直接扫描一次height .加个是不是在一个串中的推断就能够了. ...

  4. UVA 12206 - Stammering Aliens(后缀数组)

    UVA 12206 - Stammering Aliens 题目链接 题意:给定一个序列,求出出现次数大于m,长度最长的子串的最大下标 思路:后缀数组.搞出height数组后,利用二分去查找就可以 这 ...

  5. hdu 4691 最长的共同前缀 后缀数组 +lcp+rmq

    http://acm.hdu.edu.cn/showproblem.php? pid=4691 去年夏天,更多的学校的种族称号.当时,没有后缀数组 今天将是,事实上,自己的后缀阵列组合rmq或到,但是 ...

  6. POJ 2774 后缀数组:查找最长公共子

    思考:其实很easy.就在两个串在一起.通过一个特殊字符,中间分隔,然后找到后缀数组的最长的公共前缀.然后在两个不同的串,最长是最长的公共子串. 注意的是:用第一个字符串来推断是不是在同一个字符中,刚 ...

  7. POJ1743---Musical Theme(+后缀数组二分法)

    Description A musical melody is represented as a sequence of N (1<=N<=20000)notes that are int ...

  8. 后缀数组--summer-work之我连模板题都做不起

    这章要比上章的AC自动机要难理解. 这里首先要理解基数排序:基数排序与桶排序,计数排序[详解] 下面通过这个积累信心:五分钟搞懂后缀数组!后缀数组解析以及应用(附详解代码) 下面认真研读下这篇: [转 ...

  9. 后缀数组【原理+python代码】

    后缀数组 参考:https://blog.csdn.net/a1035719430/article/details/80217267 https://blog.csdn.net/YxuanwKeith ...

随机推荐

  1. Error: could not open `C:\Program Files\Java\jre6\lib\i386\jvm.cfg')

    前些日子装了个jdk7试了试,后来做项目需要换成jdk6,安装完jdk6,设置完环境变量后出现问题.运行java -version出现Error: could not open `C:\Program ...

  2. 动态创建 Log4net 实例

    动态创建log4net 实例 根据业务类型,动态的创建日志实例,将日志写到不同目录.常见的配置文件中统一配置,不能满足需求. 引用log4net nuget安装命令: Install-Package ...

  3. Linux命令-网络命令:setup

    setup 进入设置网络信息的界面 上图中选“网络配置”进入设置网络信息 上图中选“设备配置”进行配置IP地址,下图显示网卡信息 上图中,选择“eth0”回车进入设置eth0网卡信息界面 上图中选择“ ...

  4. iOS小知识点记录

    1.创建视图的两种方法:用代码创建视图,创建XIB文件.如何决定使用哪种方法?参考法则:如果视图没有子视图,就用代码创建:如果有子视图,就通过XIB文件创建. 2.创建视图的时候,视图控制器会调用lo ...

  5. java基础讲解07-----数组

    1.什么是数组 2.怎么使用数组 package test; public class ShuZu {            public static void main(String[] args ...

  6. EF、MySQL、MVC、WebAPI2 swagger 集成

    “好记星不如烂笔头",这句话一直伴随我多年,想当年还是我语文老师常用的口头禅. 时间一晃3年过去了.以前只是记得自己去看别人的博客园.时间不久自己也开通了博客园来玩玩.顺便吧自己学的记录下来 ...

  7. 兼容placeholder

    众所周知.IE9以下不兼容placeholder属性,因此自己定义了一个jQuery插件placeHolder.js.以下为placeHolder.js的代码: /** * 该控件兼容IE9下面,专门 ...

  8. 基于maven的ssh框架一步一步搭建(一)

    一.新建maven项目,配置ssh框架pom的最低支持 1.新建一个maven项目 2.添加一个web.xml ? 1 2 3 4 5 6 7 8 9 <?xml version="1 ...

  9. window下安装Node.js NPM

    一.安装Node.js 下载地址:http://nodejs.org/download/ 1..msi文件,直接安装,包括了npm,结束. 2..exe文件,把node.exe所在主目录,加入到系统P ...

  10. 优化数据页面(22)——n:n的数据关系

    设计要点:优化数据页面.界面设计.美化exce 阿金:那n::n就复杂了,你倒是想留有空间. 可是现实社会有时却不同意. 秀秀:唉.说的也是. 阿金:那怎么表达才合适啊? 秀秀:仅仅实用网格了. 阿金 ...