转自: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. Tomcat_启动参数设置

    1.修改启动时内存参数.并指定JVM时区 (在windows server 2008 下时间少了8个小时): 在Tomcat上运行j2ee项目代码时,经常会出现内存溢出的情况,解决办法是在系统参数中增 ...

  2. 【Shiro】Apache Shiro架构之集成web

    Shiro系列文章: [Shiro]Apache Shiro架构之身份认证(Authentication) [Shiro]Apache Shiro架构之权限认证(Authorization) [Shi ...

  3. App登录注册功能,怎样做到用户体验最佳?

    用户登录系统,可以细分为三项功能模块,分别是:登录.注册和密码找回.本文作者将结合自身经历,谈谈他在做这块的时候一些想法,主要是涉及业务流程. 登录和注册功能,不论是PC端还是移动端,大多数产品都会涉 ...

  4. 用Jquery控制文本框只能输入数字和字母及jquery自定义方法$.fn

    封装成onlyNum(),onlyAlpha()和onlyNumAlpha()3个Jquery扩展方法,方便复用,由于里面一些JS代码涉及到了"禁用输入法,获取剪切板的内容",而& ...

  5. Windows Azure Platform 性能监视器(转载)

    Windows操作系统提供了查看性能监视器的功能,用于监视CPU使用率.内存使用率,硬盘读写速度,网络速度等.您可以在开始-->运行-->输入Perfmon,就可以打开性能监视器. 我们知 ...

  6. php get_called_class()函数与get_class函数的区别

    get_class (): 获取当前调用方法的类名: get_called_class():获取静态绑定后的类名: 有例为证: class Foo{ public function test(){ v ...

  7. MYSQL备份与恢复精华篇

    数据备份原理 数据备份属于数据容灾保护中的内容,所有的数据备份系统设计都基于这五个元素,备份源.备份目标.传输网络.备份引擎和备份策略.用户按照需要制定备份策略,使用定时任务执行备份脚本,使用备份引擎 ...

  8. vsftp 虚拟用户高级设置(转载)

    发布:xiaokk   来源:net     [大 中 小] vsftp 虚拟用户高级设置  本文转自:http://www.jbxue.com/article/1724.html 1.安装所需软件包 ...

  9. Atitit.js模块化 atiImport 的新特性javascript import

    Atitit.js模块化 atiImport 的新特性javascript import 1. 常见的js import规范amd ,cmd ,umd1 1.1. Require更多流行3 2. at ...

  10. Atitit.index manager api design 索引管理api设计

    Atitit.index manager api design 索引管理api设计 1. kw1 1.1. 索引类型 unique,normal,fulltxt1 1.2. 聚集索引(clustere ...