学之前个人觉得还是应该看一下罗穗骞的后缀数组的国家集训队论文,虽然一开始很难看懂(反正我基本上是完全没看懂,现在距离我第一次看那篇文章也过去了1个多月,看了很多别人的论文,现在个人感觉也只是明白了一个大概),但是能理解一个大概
一个数组 s ,长度为len 那么我们总是用suffix(i) 表示从i开始一直到结尾结束,也即i的后缀
因为每个下标的后缀长度绝对不一致,那么每一个后缀都利用字典序排列,所得到的排名 是绝对不会存在一致性的
后缀数组 sa[i] 表示排名第i位的后缀的起始下标 rank[i] 表示第i位的后缀排多少名 很容易得到公式sa[rank[i]] = i 那么我们只要求出sa[],那么rank[]的求解就迎刃而解了
这里就讲讲自己对倍增算法的理解 很显然,sa[]数组的求解不能靠蛮力解决
可以这么考虑,对于字符串之间的大小比较,如果前面已经区分出了大小, 其实后面是没必要再进行比较的
我们可以不断每个下标处标记出当前所取长度时的排名 那么我们初始所取长度为1,而这里倍增算法的意思是不断将这个长度*2, 也就是前 l 的长度 和 后 l 的长度结合形成了一个新的 2*l 长度的字符
结合的时候有2个排名的标记,称之为第一关键字和第二关键字 根据这两个关键字排序后结合得到新的排名,再将此排名保存下来
倍增算法停止的位置就是在每个后缀下标的排名都不一致时结束,因为这时候 已经达到目的了,后面的比较是毫无意义的
1 int wa[maxn] , wb[maxn] , wv[maxn] , ws[maxn]; 2 3 //这里相当于是判断以a,b开头得到长度为2*l的部分,是不是同时第一关键字和第二关键字都相等 4 //所以应该像论文中那样给末尾添加一个关键字0,防止数组越界,而且0也说明末尾为空,字符串最靠前 5 int cmp(int *r , int a , int b , int l) 6 { 7 return r[a] == r[b] && r[a+l] == r[b+l]; 8 } 9 /* 10 函数中m表示字符中所能表示的大小范围,如果均为字符,转化为ASCII码,m的最大范围就是128 11 但是如果数组中保存的是整数,就要另外取值 12 */ 13 void da(int *r , int *sa , int n , int m) 14 { 15 int i,j,p,*x=wa,*y=wb,*t; 16 //接下来的就是普通的计数排序,初始化长度为1时的排名,不懂得可以百度去看看 17 for(i=0 ; i<m ; i++) ws[i]=0; 18 /* 19 这里x[]保存的各个后缀的rank值,值得思考的是初始时是可以直接用它当前的值代替, 20 因为初始长度为1,那么当前的值的大小可以作为相对排名,直接利用同样可以得到正确的 21 关键字的排序,就省略了将这些相对大小转化成1,2,3,4,...这样的排名的必要 22 */ 23 for(i=0; i<n ; i++) ws[x[i]=r[i]]++; 24 for(i=0; i<m ; i++) ws[i]=ws[i-1]++; 25 //下方i递减可以让相等的数原先若是在前面就继续排前面 26 for(int i=n-1 ; i>=0 ; i--) sa[--ws[x[i]]]=i; 27 28 /* 29 接下来开始不断倍增长度 j , p统计当前所能达到的rank值 30 那么按前面所说,p达到n时就可以退出循环了 31 */ 32 /*这里m=p表示这里在初始化结束后,其实相对值已经转化为绝对的大小比较了 33 那么数据范围就只在1~p之间 34 y[i]保存第二关键字排名第i位的在y[i]的下标处的后缀 35 */ 36 for(j=1 , p=1 ; p<n ; j*=2,m=p) 37 { 38 /*这里从i=n-j开始,是因为从这个点开始它根本就没有长为j的 39 作为第二关键字的后缀,也就是说,总长为2*j,它的后j位为空 40 我们就直接可以理解为它是最小的,所以依次保存其名次*/ 41 for(p=0 , i=n-j; i<n ; i++) y[p++]=i; 42 /*不存在后j位处理了以后,就可以按照之前得到的排名了,来安排第二关键字的排序 43 所以i表示原先的名次,不断去找到这个名次的后缀,保证其要长于j是因为这是后半部分, 44 它的前面还有第一关键字,必须保证前面还有j个位置给第一关键字的字符串才能表示这个 45 下标作为第二关键字是合法的 46 */ 47 for(i=0 ; i<n ; i++) if(sa[i]>=j) y[p++]=sa[i]-j; 48 //用一个数组暂时的保存第二关键字第i名的对应的第一关键字的值 49 for(i=0 ; i<n ; i++) wv[i]=x[y[i]]; 50 //就是按计数排序的方法对第1关键字排序 51 for(i=0 ; i<m ; i++) ws[i]=0; 52 for(i=0 ; i<n ; i++) ws[wv[i]]++; 53 for(i=1 ; i<m ; i++) ws[i] += ws[i-1]; 54 for(i=n-1 ; i>=0 ; i--) sa[--ws[wv[i]]]=y[i]; 55 56 /* 57 t=x , x=y , y=t,这里利用指针的地址交换,避免了一个个的将数组中的 58 数据复制到另一个数组中,相当于只是通过简单的改了地址的名字实现了数组 59 间的数据交换 60 61 根据新得到的排名关系不断判断前后两名是不是其实2个关键字都是相同的 62 相同则当前设为p-1也就是和上一名的排名相同 63 否则排名增加一位 64 */ 65 for(t=x , x=y , y=t , p=1 , x[sa[0]]=0 , i=1 ; i<n ; i++) 66 x[sa[i]]=cmp(y,sa[i-1],sa[i],j)?p-1:p++; 67 } 68 return ; 69 }
后缀数组中只是利用一个sa[]数组是很难解决问题的 当然rank[]通过rank[sa[i]]=i得到也是不够的
还要引进height[] height[i] 表示 suffix(sa[i-1]), suffix(sa[i])的公共前缀长度,也就是排名相连 的两个后缀的最长公共前缀
这里需要引进一个定义存在下标j,k 假设rank[j]<rank[k] 那么suffix(j) , suffix(k)的公共前缀是 height[ran[j]+1] , height[ran[j]+2] ... , height[rank[k]]中的最小值 (好吧,我承认我还是不知道为什么) 那么这里高效的求出height[]显得至关重要
对于height[]数组的求解就利用一个h[]来理解 h[i]表示suffix(i) 和 suffix(sa[rank[i]-1])的公共前缀 其实可以看出h[i] = height[rank[i]]
h数组性质的证明: h[i] >= h[i-1]-1 假设i-1的前一名下标为k那么 h[i-1] 就是suffix(i-1),suffix(k)的公共前缀
1. 假设这个前缀存在 h[i-1] > 0 , 那么r[i-1]=r[k] , 而rank[k]>rank[i-1], 除去最前面的相同的数,那么rank[k+1]>rank[i] 依旧成立 那么suffix(k+1) 和 suffix(i) 的公共前缀就定义成 val = h[i-1]-1 但是我们不能保证 k+1 就是 i 的前一名,但是val = min(height[rank[i]+1] ... height[rank[k]])的 而h[i]存在于其中也就是height[rank[i]+1],那么必然大于等于val也就是h[i-1]-1的 所以第一种情况这个性质成立
2. 假设这个前缀不存在 h[i-1] = 0 , 而h[] 是个非负整数,h[i-1]-1=-1必然小于h[i] 所以第二种情况性质依然成立
我们就可以利用这个性质在线性的时间内求得height[]
1 int rank[maxn] , height[maxn]; 2 void callheight(int *r , int *sa , int n) 3 { 4 int i , j , k=0; 5 for(i=1;i<=n;i++) rank[sa[i]]=i; 6 /* 7 若k存在,就k--表示将第一位相同的数据抵消了,否则依然保持0 8 然后从当前已知的能到达的最远的位置不断向后判断两个位置上对应的 9 值是否相等,相等那么k就不断加1 10 */ 11 for(i=0;i<n;height[rank[i++]]=k) 12 for(k?k-- , j=sa[rank[i]-1] ; r[i+k]==r[j+k] ; k++); 13 14 return; 15 }