后缀数组的倍增算法(Prefix Doubling)
最近在自学BWT算法(Burrows-Wheeler transform),其中涉及到对字符串循环移位求编码。直观的办法就是模拟,使用O(n3)的时间求出BWT编码。经过简单的简化后也要O(n2logn)的时间,显然当字符串长度很大时这种方法的效率很低。
由于循环移位的结果类似后缀(二者有所不同,所以在字符串结尾添加了一个字典序严格小于所有字符的符号,例如'\0',使得循环移位的有效部分等同于后缀),因此可以使用后缀树或后缀数组的方式优化BWT的过程。
关于学习倍增算法,你应该:
- 理解朴素的后缀数组生成方法
- 理解基数排序(本文使用了基数排序,至于原始的倍增算法是否是使用基数排序本人也不清楚)
- 了解KMP算法的原理
先来谈谈KMP算法。它之所以能有效减少比对次数是因为它利用了之前比对的结果——利用前缀的自相似性跳过必然失败的匹配,直接进行有可能成功的尝试。
而倍增算法同样拥有类似的思想,例如cake拥有后缀
cake
ake
ke
e
当我们比较了每个后缀第一个字母后(2nd 1st 4th 3rd),实际上我们也知道了每个后缀的第二个字母的比较结果(1st 4th 3rd -)。类似的,后续结果也就知道了。因此,我们可以得到逐步扩展每个后缀的前缀比较结果(2 1 4 3)->(21 14 43 3-)->(214 143 43- 3--)……参考图1(本例和图中所示不同,但思路是一样的)
上述延伸过程是线性增加的。若是再贪心一点,则可以利用上一回的比较结果将该回的前缀比较长度增加一倍,即指数级增长。这也就是倍增算法的核心思路。

图1引用自NOCOW
再来谈谈利用基数排序的算法实现。基数排序分为LSD(Least significant digital)和MSD(Most significant digital)两大类。乍一看后缀数组的比较是从高位开始的(p.s. 为什么不从低位开始呢?删除一个整数而不改变相对大小关系很简单,但添加一个整数而不改变相对大小关系比较麻烦),很适合MSD。但MSD的时间开销随序列复杂度和长度增长很快,仅适用于短序列,所以LSD是个无奈之选。可以说,倍增算法的代码之所以晦涩很大一部分原因就是使用LSD的缘故。
code in C++
#include<stdio.h>
#include<string.h>
#define rank r_sa
const int MAXN=21;
char str[MAXN];
int sa[MAXN];//suffix array
int l_sa[MAXN];//low of sa
int r_sa[MAXN];//reverse mapping of sa, also known as rank array
int t_r_sa[MAXN];//temperary copy of r_sa
char BWT[MAXN];
int c[MAXN+128];//数组长度必须大于字符串长度和字符总数的最大值
bool sa_cmp(int *r,int sa1,int sa2,int j){
//此处完美地进行了越界判断
return (r[sa1]==r[sa2] && r[sa1+j]==r[sa2+j]);
}
int prefixdouble(char *s,int l){
int i,j,k,m;
//对后缀的第一位进行基数排序
memset(c,0,sizeof(c));
for(i=0;i<l;i++)
c[ s[i] ]++;
for(m=1;m<MAXN+128;m++)
c[m]+=c[m-1];
for(i=l-1;i>=0;i--)
sa[ --c[s[i]] ]=i;
//r_sa[i]=k 即第i个后缀排名第k
for(i=0;i<l;i++)
r_sa[i]=s[i];//此时仅需反映相对大小顺序
int p;
for(j=1;j<=l;j*=2){
//由于采用LSD,先对低位进行排序
p=0;
//l_sa[k]=i 即排名第k的是第i个后缀
for(i=l-j;i<l;i++)
l_sa[p++]=i;//长度小于j的后缀无低位关键字,直接排在最前
for(k=0;k<l;k++)
if(sa[k]>=j) l_sa[p++]=sa[k]-j;//第i-j个后缀的低位关键字等于第i个后缀的高位关键字,并且高位关键字在之前已有序
//再对高位进行排序
memset(c,0,sizeof(c));
for(k=0;k<l;k++)
c[ r_sa[ l_sa[k] ] ]++;
for(m=1;m<MAXN+128;m++)
c[m]+=c[m-1];
for(k=l-1;k>=0;k--)
sa[ --c[ r_sa[ l_sa[k] ] ] ]=l_sa[k];
//更新r_sa
memcpy(t_r_sa,r_sa,4*MAXN);
r_sa[ sa[0] ]=p=0;
//相邻后缀如果前缀相同,那么其rank也相同
for(k=1;k<l;k++)
r_sa[sa[k]]=sa_cmp(t_r_sa,sa[k-1],sa[k],j)?p:++p;
if(p==l-1) break;
}
/*test
for(k=0;k<l;k++)
printf("%2d:%s\n",k,s+sa[k]);
BWT[0]=s[l-2];
for(i=1;i<l;i++)
BWT[i]=s[sa[i]-1];
BWT[l]='\0';
printf("trans:");
for(i=0;i<l;i++)
printf("%c",BWT[i]);
*/
}
int main(){
printf("The string inputed should short than 20 symbols.\n");
scanf("%s",str);
int l=strlen(str);
prefixdouble(str,l+1);
for(int i=0;i<l;i++)
printf("%d ",sa[i]);
return 0;
}
后缀数组的倍增算法(Prefix Doubling)的更多相关文章
- 后缀数组:倍增法和DC3的简单理解
一些定义:设字符串S的长度为n,S[0~n-1]. 子串:设0<=i<=j<=n-1,那么由S的第i到第j个字符组成的串为它的子串S[i,j]. 后缀:设0<=i<=n- ...
- 关于后缀数组的倍增算法和height数组
自己看着大牛的论文学了一下后缀数组,看了好久好久,想了好久好久才懂了一点点皮毛TAT 然后就去刷传说中的后缀数组神题,poj3693是进化版的,需要那个相同情况下字典序最小,搞这个搞了超久的说. 先简 ...
- 【HDOJ6223】Infinite Fraction Path(后缀数组,倍增)
题意: 给一个长度为n的字符串s[0..n-1],但i的后继不再是i+1,而是(i*i+1)%n,求所有长度为n的“子串”中,字典序最大的是谁 n<=150000,s[i]=0..9 思路:后缀 ...
- 笔试算法题(40):后缀数组 & 后缀树(Suffix Array & Suffix Tree)
议题:后缀数组(Suffix Array) 分析: 后缀树和后缀数组都是处理字符串的有效工具,前者较为常见,但后者更容易编程实现,空间耗用更少:后缀数组可用于解决最长公共子串问题,多模式匹配问题,最长 ...
- 后缀树 & 后缀数组
后缀树: 字符串匹配算法一般都分为两个步骤,一预处理,二匹配. KMP和AC自动机都是对模式串进行预处理,后缀树和后缀数组则是对文本串进行预处理. 后缀树的性质: 存储所有 n(n-1)/2 个后缀需 ...
- 【后缀数组之SA数组】【真难懂啊】
基本上一搜后缀数组网上的模板都是<后缀数组——处理字符串的有力工具>这一篇的注释,O(nlogn)的复杂度确实很强大,但对于初次接触(比如窝)的人来说理解起来也着实有些困难(比如窝就活活好 ...
- 后缀数组--summer-work之我连模板题都做不起
这章要比上章的AC自动机要难理解. 这里首先要理解基数排序:基数排序与桶排序,计数排序[详解] 下面通过这个积累信心:五分钟搞懂后缀数组!后缀数组解析以及应用(附详解代码) 下面认真研读下这篇: [转 ...
- HDU 4691 正解后缀数组(暴力也能过)
本来是个后缀数组,考察算法的中级题目,暴力居然也可以水过,就看你跳不跳坑了(c++和G++返回结果就很不一样,关键看编译器) 丝毫不差的代码,就看运气如何了.唯一差别c++还是G++,但正解是后缀数组 ...
- 利用后缀数组(suffix array)求最长公共子串(longest common substring)
摘要:本文讨论了最长公共子串的的相关算法的时间复杂度,然后在后缀数组的基础上提出了一个时间复杂度为o(n^2*logn),空间复杂度为o(n)的算法.该算法虽然不及动态规划和后缀树算法的复杂度低,但其 ...
随机推荐
- ZIP压缩算法详细分析及解压实例解释
最近自己实现了一个ZIP压缩数据的解压程序,觉得有必要把ZIP压缩格式进行一下详细总结,数据压缩是一门通信原理和计算机科学都会涉及到的学科,在通信原理中,一般称为信源编码,在计算机科学里,一般称为数据 ...
- 【小计】新人Tostring前忘记Null判断的处理
ToString和string.Concat(可屏蔽Null的异常)性能相差不大,一些中小项目完全可以用Concat(新人容易忘记判断Null的情况,遇到太多了,所以建议重写tostring方法,内部 ...
- JavaScript Date对象
本篇主要介绍 Date 日期和时间对象的操作. 目录 1. 介绍:阐述 Date 对象. 2. 构造函数:介绍 Date 对象的构造函数new Date()几种方式. 3. 实例方法:介绍 Date ...
- 前端常用的WindowsCMD命令
前面的话 在网上找了一些关于命令提示符CMD的资料,但是很多资料都是把所有的功能罗列出来,大部分都不会用到.所以,自己把常用的CMD命令总结如下,方便查阅 操作类 help 列出所有支持的指令及说 ...
- CentOS7 重置root密码
1- 在启动grub菜单,选择编辑选项启动 2 - 按键盘e键,来进入编辑界面 3 - 找到Linux 16的那一行,将ro改为rw init=/sysroot/bin/sh 4 - 现在按下 Con ...
- iOS开发之Masonry框架源码深度解析
Masonry是iOS在控件布局中经常使用的一个轻量级框架,Masonry让NSLayoutConstraint使用起来更为简洁.Masonry简化了NSLayoutConstraint的使用方式,让 ...
- 《JavaScript设计模式与开发实践》整理
最近在研读一本书<JavaScript设计模式与开发实践>,进阶用的. 一.高阶函数 高阶函数是指至少满足下列条件之一的函数. 1. 函数可以作为参数被传递. 2. 函数可以作为返回值输出 ...
- 【Big Data】HADOOP集群的配置(一)
Hadoop集群的配置(一) 摘要: hadoop集群配置系列文档,是笔者在实验室真机环境实验后整理而得.以便随后工作所需,做以知识整理,另则与博客园朋友分享实验成果,因为笔者在学习初期,也遇到不少问 ...
- HIVE教程
完整PDF下载:<HIVE简明教程> 前言 Hive是对于数据仓库进行管理和分析的工具.但是不要被“数据仓库”这个词所吓倒,数据仓库是很复杂的东西,但是如果你会SQL,就会发现Hive是那 ...
- 使用EF CodeFirst 创建数据库
EntityFramework 在VS2015添加新建项时,选择数据->ADO.NET 实体数据模型,有一下选项 来自数据库的EF设计器,这个就是我们最常用的EntityFramework设计模 ...