【后缀数组之SA数组】【真难懂啊】
基本上一搜后缀数组网上的模板都是《后缀数组——处理字符串的有力工具》这一篇的注释,O(nlogn)的复杂度确实很强大,但对于初次接触(比如窝)的人来说理解起来也着实有些困难(比如窝就活活好了两天的光阴。。),看了那么多材料感觉《挑战程序设计》的后缀数组解释理解起来会相对容易很多,然而它的复杂度是O(nlog2n)的,主要区别是对子串排序的时候前者用了计数排序--O(n),而后者用了快排--O(nlogn),这就导致了最终的复杂度后者比前者多了一个O(logn)。
O(nlog2n)算法
先附清爽版求SA(Suffix_Array)模板,主要思想当然仍是倍增法;
#include <iostream> #include <cstring> #include <cstddef> #include <cstdio> #include <string> #include <algorithm> int wa[maxn],wb[maxn],wv[maxn],ws[maxn]; int cmp(int *rank, int a,int b,int l) { return rank[a]==rank[b] && rank[a+l]==rank[b+l]; } void DA(int *r,int *sa,int n,int m) { int i, k, p, *x=wa, *y=wb, *t; for(i=0;i<m;i++) ws[i] = 0; for(i=0;i<n;i++) ws[x[i] = r[i]]++; for(i=1;i<m;i++) ws[i] += ws[i-1]; for(i=n-1;i>=0;i--) sa[--ws[x[i]]] = i; for(k=1, p=1; p<n; k*=2, m=p) { p=0; for(i=n-k;i<n;i++) y[p++]=i; for(i=0;i<n;i++) if(sa[i]>=k) y[p++]=sa[i]-k; for(i=0;i<n;i++) wv[i]=x[y[i]]; for(i=0;i<m;i++) ws[i]=0; for(i=0;i<n;i++) ws[wv[i]]++; for(i=1;i<m;i++) ws[i]+=ws[i-1]; for(i=n-1;i>=0;i--) sa[--ws[wv[i]]]=y[i]; for(t=x,x=y,y=t,p=1,x[sa[0]]=0,i=1;i<n;i++) x[sa[i]]=cmp(y,sa[i-1],sa[i],k)?p-1:p++; } return; } DA(r,sa,n+1,128);View Code
总觉得把整个代码和注释混一起看好烦躁,分解开来一部分一部分看吧~
定义:
int wa[maxn],wb[maxn],wv[maxn],ws[maxn]; /* wa[]: 本意是保存各个后缀的rank值的,但是这里并没有去存储rank值,因为后续只是涉及wa[]的比较工作, 因而这一步可以不用存储真实的rank值,能够反映相对的大小即可。 wb[]: 存放的是按第二关键字排序的子串首字符下标 wv[]: 存放每个子串的第一关键字 ws[]: 存放每个rank值的数目 */
函数:
void da(int *r,int *sa,int n,int m) { ... } /* *r: 字符串(数组) *sa: 后缀数组 n: 字符串中字符的个数,注意这里的n里面是包括人为在字符串末尾添加的那个0的 m: 字符串中字符的取值范围,是基数排序的一个参数,如果原序列都是字母可以直接取128,如果原序列本身都是整数的话,则m可以取比最大的整数大1的值。 */
int i, k, p, *x=wa, *y=wb, *t; /* *x 代替wa数组,*y 代替wb数组 *t 作交换指针 */
以下四行代码是把长度为1的子串进行基数排序 Tips: 如果不理解为什么这样可以达到计数排序的效果,建议自己实际用纸笔模拟一下!
for(i=0;i<m;i++) ws[i] = 0; for(i=0;i<n;i++) ws[x[i] = r[i]]++; for(i=1;i<m;i++) ws[i] += ws[i-1]; for(i=n-1;i>=0;i--) sa[--ws[x[i]]] = i; /*
ws[]: 存放每个rank值的数目; 第1行,清零; 第2行,上面已经提到x[]保存的是后缀的相对rank值,x[i]=r[i]的意思是将x[i]初始化为各字符的ASCII值,字符的ASCII值也就可以代表长度为1的子串的相对顺序; 第3行,求出最后一个子串i的rank是多少,供第4行使用 第4行,相当于从后向前得到各子串的sa[]数组,i之所以从n-1开始循环,是为了保证在当字符串中有相等的字符串时,默认靠前的字符串更小一些。 */
注意理解上述“相对顺序”的含义,上一部分也提到过这个概念,我们并不需要求出子串的排序到底是1还是2,我们只需要达到若r[i]<r[j],则x[i]<x[j]的要求即可。这也解释了利用字符的ASCII值初始x[]数组的合理性;
主要循环:
for(k=1, p=1; p<n; k*=2, m=p) { ... } /* 这层循环中p的值表示的是此时关键字不同的子串的数量,也可以这么理解,所有子串排序后,相等的子串rank值相同,则关键字的范围是[1,p]; 如果p达到n,即各后缀的rank与sa 已全部求出,因为后缀长度不一,所以不可能出现相等的情况; k代表当前待合并的字符串的长度,每次将两个长度为k的字符串合并成一个长度为2k的字符串——倍增思想; m同样代表基数排序的元素的取值范围 */
以下两行代码实现对第二关键字的排序
此处我们需要借助罗神的插图来理解了
我们可以这么理解所谓第二关键字 (为避免混淆,原字符串r换为字母s来表示)
首字符为i长度为1的子串没有第二关键字
首字符为i长度为2的子串s[i,2]的第一关键字为s[i,1]的rank值,第二关键字为s[i+1,1]的rank值;
首字符为i长度为4的子串s[i,4]的第一关键字为s[i,2]的rank值,第二关键字为s[i+2,2]的rank值;
...
首字符为i长度为2k的子串s[i,2k]的第一关键字为s[i,k]的rank值,第二关键字为s[i+k,k]的rank值;
由图2,可以看到首字符下标为n-k至n的子串的第二关键字都为0,因此如果按第二关键字排序,必然这些子串都是排在前面的。(第二关键字为0即无法构成以r[i]为首字符的长度为2k的子串,长度都不够,自然字典序会小咯)—— 第1个循环。
我们还可以看到,下面一行的第二关键字的值(非0)都是上一轮的rank值,且上一行中只有首字符下标(sa[i])>=k的子串的rank值才会作为下一行的首字符下标为sa[i]-j的子串的第二关键字,而且显然按sa[i]的顺序rank[sa[i]]是递增的(rank[sa[i]] == i) —— 第2个循环。
图2的y[]值为
k: 1时
y[0] : 8
y[1] : 7
y[2] : 0
y[3] : 2
y[4] : 3
y[5] : 4
y[6] : 5
y[7] : 1
y[8] : 6
k: 2时
y[0] : 7
y[1] : 8
y[2] : 6
y[3] : 1
y[4] : 2
y[5] : 3
y[6] : 4
y[7] : 5
y[8] : 0
y[i] = x的含义是按第二关键字排序后第i小的子串首字符下标为x;
记住y[]里存放的是按第二关键字排序的子串首字符下标!
p=0; for(i=n-k;i<n;i++) y[p++]=i; for(i=0;i<n;i++) if(sa[i]>=k) y[p++]=sa[i]-k;
for(i=0;i<n;i++) wv[i]=x[y[i]]; /*这里相当于提取出每个子串的第一关键字(前面说过了x[]是保存上一轮的rank值的,也就是子串的第一关键字),放到wv[]里面是方便后面的使用*/
以下四行代码是按第一关键字进行的基数排序
wv[]: 存放每个长度为k的子串的第一关键字,wv[i] = x的含义为按第二关键字第i小的子串的第一关键字的值;
ws[]: 存放每个关键字的个数
for(i=0;i<m;i++) ws[i]=0; for(i=0;i<n;i++) ws[wv[i]]++; for(i=1;i<m;i++) ws[i]+=ws[i-1]; for(i=n-1;i>=0;i--) sa[--ws[wv[i]]]=y[i]; /*i之所以从n-1开始循环,含义同上,同时注意这里是y[i],因为y[i]里面才存着字符串的下标*/
最后一行巧妙地将第一关键字与第二关键字结合起来了,注意理解!
下面两行就是计算合并之后的rank值了,而合并之后的rank值应该存在x[]里面,但我们计算的时候又必须用到上一层的rank值,也就是现在x[]里面放的东西,如果我既要从x[]里面拿,又要向x[]里面放,怎么办?
当然是先把x[]的东西放到另外一个数组里面,省得乱了。这里就是用交换指针的方式,高效实现了将x[]的东西“复制”到了y[]中。
t=x,x=y,y=t; for(p=1,x[sa[0]]=0,i=1;i<n;i++) x[sa[i]]=cmp(y,sa[i-1],sa[i],k)?p-1:p++; /* 这里就是用x[]存储计算出的各字符串rank的值了,记得我们前面说过,计算sa[]值的时候如果字符串相同是默认前面的更小的,但这里计算rank的时候必须将相同的字符串看作有相同的rank,要不然p==n之后就不会再循环啦 p的值表示的是此时关键字不同的串的数量 cmp比较函数,合并的子串相同则返回1,不同返回0; 注意p和i的初始值需为1,因为循环中存在i-1和p-1,而x[sa[0]]的值也需初始化为0 */
选计数排序一个重要的原因,它是一个稳定排序,这就保证了数组的下标是第二关键字,我们前面说了,对于倍增长度k,利用之前排序k/2长度后得到的rank数组作为关键字,把后k/2部分作为第二关键字,嗯,就是这里,所以我们要先排后k/2的序,然后得到新的数组序列,下标就是第二关键字了,数组里面就是前k/2 rank的值,这是第一关键字,那么直接排序就相当于先对前k/2排序,如果这里相等,那么就会按下标排序,即第二关键字排序。
- nginx+tomcat集群负载均衡下tomcat故障后的的会话转移
- 生成注释
- 为什么匿名内部类参数必须为final类型
- 百度牵手大悦城 相爱相杀的零售与互联网需要新玩法
- 在textView中输入时避免键盘的遮挡
- 简单的邮件发送
- Linux下基于HTTP协议带用户认证的GIT开发环境设置
- 第一次作业
- fopen与读写的标识r,r+,rb+,rt+,w+.....
- Reveal 编译错误 Undefined symbols for architecture i386
- nginx负载均衡基于ip_hash的session粘帖
- VirturalBox中安装redhat9注意点
- 互联网世界的 “人工智能”——探秘 “深度学习” 的前世今生
- MongoDB内存使用原理
- PL/SQL Developer,大小写转换
- 获取两个时间段(格式:2015-08-27),求出两个时间点相差的年月日
- Oracle 查询耗时 SQL
- java基础-IO-File类、Properties类、打印流、序列流、字符编码
- 简单的欢迎界面,延时播放
- HTTP get方式调用接口