最近在自学BWT算法(Burrows-Wheeler transform),其中涉及到对字符串循环移位求编码。直观的办法就是模拟,使用O(n3)的时间求出BWT编码。经过简单的简化后也要O(n2logn)的时间,显然当字符串长度很大时这种方法的效率很低。

由于循环移位的结果类似后缀(二者有所不同,所以在字符串结尾添加了一个字典序严格小于所有字符的符号,例如'\0',使得循环移位的有效部分等同于后缀),因此可以使用后缀树或后缀数组的方式优化BWT的过程。


关于学习倍增算法,你应该:

  1. 理解朴素的后缀数组生成方法
  2. 理解基数排序(本文使用了基数排序,至于原始的倍增算法是否是使用基数排序本人也不清楚)
  3. 了解KMP算法的原理

先来谈谈KMP算法。它之所以能有效减少比对次数是因为它利用了之前比对的结果——利用前缀的自相似性跳过必然失败的匹配,直接进行有可能成功的尝试。

而倍增算法同样拥有类似的思想,例如cake拥有后缀

  1. cake
  2. ake
  3. ke
  4. 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++

  1. #include<stdio.h>
  2. #include<string.h>
  3. #define rank r_sa
  4. const int MAXN=21;
  5. char str[MAXN];
  6. int sa[MAXN];//suffix array
  7. int l_sa[MAXN];//low of sa
  8. int r_sa[MAXN];//reverse mapping of sa, also known as rank array
  9. int t_r_sa[MAXN];//temperary copy of r_sa
  10. char BWT[MAXN];
  11. int c[MAXN+128];//数组长度必须大于字符串长度和字符总数的最大值
  12. bool sa_cmp(int *r,int sa1,int sa2,int j){
  13. //此处完美地进行了越界判断
  14. return (r[sa1]==r[sa2] && r[sa1+j]==r[sa2+j]);
  15. }
  16. int prefixdouble(char *s,int l){
  17. int i,j,k,m;
  18. //对后缀的第一位进行基数排序
  19. memset(c,0,sizeof(c));
  20. for(i=0;i<l;i++)
  21. c[ s[i] ]++;
  22. for(m=1;m<MAXN+128;m++)
  23. c[m]+=c[m-1];
  24. for(i=l-1;i>=0;i--)
  25. sa[ --c[s[i]] ]=i;
  26. //r_sa[i]=k 即第i个后缀排名第k
  27. for(i=0;i<l;i++)
  28. r_sa[i]=s[i];//此时仅需反映相对大小顺序
  29. int p;
  30. for(j=1;j<=l;j*=2){
  31. //由于采用LSD,先对低位进行排序
  32. p=0;
  33. //l_sa[k]=i 即排名第k的是第i个后缀
  34. for(i=l-j;i<l;i++)
  35. l_sa[p++]=i;//长度小于j的后缀无低位关键字,直接排在最前
  36. for(k=0;k<l;k++)
  37. if(sa[k]>=j) l_sa[p++]=sa[k]-j;//第i-j个后缀的低位关键字等于第i个后缀的高位关键字,并且高位关键字在之前已有序
  38. //再对高位进行排序
  39. memset(c,0,sizeof(c));
  40. for(k=0;k<l;k++)
  41. c[ r_sa[ l_sa[k] ] ]++;
  42. for(m=1;m<MAXN+128;m++)
  43. c[m]+=c[m-1];
  44. for(k=l-1;k>=0;k--)
  45. sa[ --c[ r_sa[ l_sa[k] ] ] ]=l_sa[k];
  46. //更新r_sa
  47. memcpy(t_r_sa,r_sa,4*MAXN);
  48. r_sa[ sa[0] ]=p=0;
  49. //相邻后缀如果前缀相同,那么其rank也相同
  50. for(k=1;k<l;k++)
  51. r_sa[sa[k]]=sa_cmp(t_r_sa,sa[k-1],sa[k],j)?p:++p;
  52. if(p==l-1) break;
  53. }
  54. /*test
  55. for(k=0;k<l;k++)
  56. printf("%2d:%s\n",k,s+sa[k]);
  57. BWT[0]=s[l-2];
  58. for(i=1;i<l;i++)
  59. BWT[i]=s[sa[i]-1];
  60. BWT[l]='\0';
  61. printf("trans:");
  62. for(i=0;i<l;i++)
  63. printf("%c",BWT[i]);
  64. */
  65. }
  66. int main(){
  67. printf("The string inputed should short than 20 symbols.\n");
  68. scanf("%s",str);
  69. int l=strlen(str);
  70. prefixdouble(str,l+1);
  71. for(int i=0;i<l;i++)
  72. printf("%d ",sa[i]);
  73. return 0;
  74. }

后缀数组的倍增算法(Prefix Doubling)的更多相关文章

  1. 后缀数组:倍增法和DC3的简单理解

    一些定义:设字符串S的长度为n,S[0~n-1]. 子串:设0<=i<=j<=n-1,那么由S的第i到第j个字符组成的串为它的子串S[i,j]. 后缀:设0<=i<=n- ...

  2. 关于后缀数组的倍增算法和height数组

    自己看着大牛的论文学了一下后缀数组,看了好久好久,想了好久好久才懂了一点点皮毛TAT 然后就去刷传说中的后缀数组神题,poj3693是进化版的,需要那个相同情况下字典序最小,搞这个搞了超久的说. 先简 ...

  3. 【HDOJ6223】Infinite Fraction Path(后缀数组,倍增)

    题意: 给一个长度为n的字符串s[0..n-1],但i的后继不再是i+1,而是(i*i+1)%n,求所有长度为n的“子串”中,字典序最大的是谁 n<=150000,s[i]=0..9 思路:后缀 ...

  4. 笔试算法题(40):后缀数组 & 后缀树(Suffix Array & Suffix Tree)

    议题:后缀数组(Suffix Array) 分析: 后缀树和后缀数组都是处理字符串的有效工具,前者较为常见,但后者更容易编程实现,空间耗用更少:后缀数组可用于解决最长公共子串问题,多模式匹配问题,最长 ...

  5. 后缀树 & 后缀数组

    后缀树: 字符串匹配算法一般都分为两个步骤,一预处理,二匹配. KMP和AC自动机都是对模式串进行预处理,后缀树和后缀数组则是对文本串进行预处理. 后缀树的性质: 存储所有 n(n-1)/2 个后缀需 ...

  6. 【后缀数组之SA数组】【真难懂啊】

    基本上一搜后缀数组网上的模板都是<后缀数组——处理字符串的有力工具>这一篇的注释,O(nlogn)的复杂度确实很强大,但对于初次接触(比如窝)的人来说理解起来也着实有些困难(比如窝就活活好 ...

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

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

  8. HDU 4691 正解后缀数组(暴力也能过)

    本来是个后缀数组,考察算法的中级题目,暴力居然也可以水过,就看你跳不跳坑了(c++和G++返回结果就很不一样,关键看编译器) 丝毫不差的代码,就看运气如何了.唯一差别c++还是G++,但正解是后缀数组 ...

  9. 利用后缀数组(suffix array)求最长公共子串(longest common substring)

    摘要:本文讨论了最长公共子串的的相关算法的时间复杂度,然后在后缀数组的基础上提出了一个时间复杂度为o(n^2*logn),空间复杂度为o(n)的算法.该算法虽然不及动态规划和后缀树算法的复杂度低,但其 ...

随机推荐

  1. Partition:Partiton Scheme是否指定Next Used?

    在SQL Server中,为Partition Scheme多次指定Next Used,不会出错,最后一次指定的FileGroup是Partition Scheme的Next Used,建议,在执行P ...

  2. JavaScript的继承实现方式

    1.使用call或apply方法,将父对象的构造函数绑定在子对象上 function A(){ this.name = 'json'; } function B(){ A.call(this); } ...

  3. 操作系统篇-hello world(免系统运行程序)

     || 版权声明:本文为博主原创文章,未经博主允许不得转载. 一.前言     今天起开始分享关于操作系统的相关知识,本人也是菜鸟一个,正处于学习阶段,这整个操作系统篇也是我边学习边总结的一些结果,希 ...

  4. c#比较两个数组的差异

    将DataTable中某一列数据直接转换成数组进行比较,使用的Linq,要引用命名空间using System.Linq; string[] arrRate = dtRate.AsEnumerable ...

  5. 星浩资本快速发展引擎:IT就是生产力

    星浩资本成立于2010年,是一家涵盖私募基金.开发管理.商业与现代服务业三大业务范围的综合性管理公司,专注于投资中国首创.高成长性.高回报率的创新型城市综合体. 年轻的星浩资本在商业投资上有其独到的商 ...

  6. 浅谈SQL注入风险 - 一个Login拿下Server

    前两天,带着学生们学习了简单的ASP.NET MVC,通过ADO.NET方式连接数据库,实现增删改查. 可能有一部分学生提前预习过,在我写登录SQL的时候,他们鄙视我说:“老师你这SQL有注入,随便都 ...

  7. Redis配置文件redis.conf

    1.地址 2.Units单位 1 配置大小单位,开头定义了一些基本的度量单位,只支持bytes,不支持bit 2 对大小写不敏感 3.includes包含

  8. 跟着老男孩教育学Python开发【第四篇】:模块

    双层装饰器示例 __author__ = 'Golden' #!/usr/bin/env python # -*- coding:utf-8 -*-   USER_INFO = {}   def ch ...

  9. FineReport:任意时刻只允许在一个客户端登陆账号的插件

    在使用FineReport报表系统中,处于账户安全考虑,有些企业希望同一账号在任意时刻智能在统一客户端登录.那么当A用户在C1客户端登陆后,该账号又在另外一个C2客户端登陆,服务器如何取判断呢? 开发 ...

  10. Android(3)—Mono For Android App版本自动更新(2)

    0.前言 这篇博文是上一篇的延续,主要是修改上一个版中的BUG和优化一些待完善的项,也算是结贴,当然还有需要完善的,等日后项目中用到的时候再单独写出来吧,本篇主要写升级改进的部分: 改进1.修复[BU ...