后缀数组之倍增算法
首先说明 :后缀数组的构建在网上有多种方法:朴素的n*n*logn,还有倍增n*logn的,还有3*n的DC3算法,当然还有DC算法。这个算法学习自林厚丛老师的《高级数据结构》,代码较长,而且常数也比较大,但是是我这种笨人可以理解的。如有人想学短而快的可以学习《罗穗骞 后缀数组 ---处理字符串的有力工具》。顺便说一下,罗大神的算法书写的的确很短小也漂亮,可惜我看不懂。
说一下学习的心路历程吧!最开始想学后缀树,道理看明的了,可是一看代码实在是太长了(可能是我找的模版不对吧)。后来看到后缀数组的功能也不错,可以实现后缀树的很多功能,于是转向后缀数组。于是向林大神学习,可是在他漂亮的代码映照下的是我愚笨的脑袋,最后是林厚丛老师救了我。感谢林老师!!!
学习前的准备:
1、后缀数组的各种基本概念
后缀:字符串中从第i个开始到它的最后一个。如字符串abcde。则bcde、cde、de、e都是他的后缀,当然他本身也是自己的后缀。
后缀数组:有两种sa数组和rank数组。
sa[i]表示把字符串的所有后缀排序后排第i的是以第几个字母开头的后缀。
rank[i]表示以第i个字母开头的后缀在后缀的排序中排第几。
2、计数排序和基数排序(可以百度一下)
计数排序也就是桶排,时间复杂度O(n)
1 #include <iostream> 2 using namespace std; 3 const int MAXN = 100000; 4 const int k = 1000; // range 5 int a[MAXN], c[MAXN], ranked[MAXN]; 6 7 int main() { 8 int n; 9 cin >> n; 10 for (int i = 0; i < n; ++i) { 11 cin >> a[i]; 12 ++c[a[i]]; 13 } 14 for (int i = 1; i < k; ++i) 15 c[i] += c[i-1]; 16 for (int i = n-1; i >= 0; --i) 17 ranked[--c[a[i]]] = a[i];//如果是i表达的是原数标号,a[i]就是排序后的正确序列 18 for (int i = 0; i < n; ++i) 19 cout << ranked[i] << endl; 20 return 0; 21 }
基数排序,也称桶子排序(注意和上面的区分),实际上是分关键安排序。首先按次关键字排,再按首关键字排。
1 int maxbit(int data[], int n) //辅助函数,求数据的最大位数 2 { 3 int d = 1; //保存最大的位数 4 int p = 10; 5 for(int i = 0; i < n; ++i) 6 { 7 while(data[i] >= p) 8 { 9 p *= 10; 10 ++d; 11 } 12 } 13 return d; 14 } 15 void radixsort(int data[], int n) //基数排序 16 { 17 int d = maxbit(data, n); 18 int *tmp = newint[n]; 19 int *count = newint[10]; //计数器 20 int i, j, k; 21 int radix = 1; 22 for(i = 1; i <= d; i++) //进行d次排序 23 { 24 for(j = 0; j < 10; j++) 25 count[j] = 0; //每次分配前清空计数器 26 for(j = 0; j < n; j++) 27 { 28 k = (data[j] / radix) % 10; //统计每个桶中的记录数 29 count[k]++; 30 } 31 for(j = 1; j < 10; j++) 32 count[j] = count[j - 1] + count[j]; //将tmp中的位置依次分配给每个桶 33 for(j = n - 1; j >= 0; j--) //将所有桶中记录依次收集到tmp中 34 { 35 k = (data[j] / radix) % 10; 36 tmp[count[k] - 1] = data[j]; 37 count[k]--; 38 } 39 for(j = 0; j < n; j++) //将临时数组的内容复制到data中 40 data[j] = tmp[j]; 41 radix = radix * 10; 42 } 43 delete[]tmp; 44 delete[]count; 45 }
3、后缀数组倍增算法的基本思想
一个字符串的所有后缀就是n(字符串的长度)个字符串,如果对它们进行排序就是n*n,由于字符串的比较要扫描串长所以时间复杂度也就成了n*n*n,如果用快排的思想n*n*logn。
倍增算法的思想:
首先用计数排序的方法对单个字符进行排序,得到按单字符进行排序后的rank[],以后的排序就是以此数组代表字符进行排序。
aabaaaaba
112111121(rank[])
单个字符很有可能是有重复的,所以要比较第二个字符。但是第二个字符的大小已经比较过了(最后一个字符开始的串没有第二个字符,所以补0)。即
abaaaaba0
121111210
这样就以第二个字符的大小为第二关键字,第一个字符的大小为第一关键字进行基数排序。得到以两个字符进行排序后的rank[]。
同样,我们可以用后面已经算好的两个字符的大小算出按4个字符排序的顺序。然后是8个、16个……。直到字符串的长度。
代码:
1 #include<cstdio> 2 #include<iostream> 3 #include<cstring> 4 #include<algorithm> 5 6 using namespace std; 7 int s[100],rank[100],sa[100];//ss:字符串,s:ss对应的数值,rank:rank数组,sa:sa数组 8 char ss[100]; 9 void build(int *st,int *sa,int *rank,int n,int mx) 10 { 11 int *cnt=new int[mx+3],*cntrank=new int[n+3];//cnt:各个字符出现的次数 12 int *rank1=new int[n+3],*rank2=new int[n+3];//关键字 13 int *tpsa=new int[n+3]; //临时sa 14 memset(cnt,0,sizeof(int)*(mx+3)); 15 for(int i=0;i<n;i++)cnt[st[i]]++; //计每个字符出现的次数 16 for(int i=1;i<=mx;i++)cnt[i]+=cnt[i-1]; //第i个字符的名次范围 17 for(int i=0;i<n;i++)rank[i]=cnt[st[i]]-1; //第i个字符的排名,到这里完成单个字符的计数排序 18 19 for(int l=1;l<n;l<<=1) //进行倍增 20 { 21 for(int i=0;i<n;i++) //取得第一、第二关键字 22 { 23 rank1[i]=rank[i]; 24 rank2[i]=i+l<n?rank[i+l]:0; 25 } 26 memset(cntrank,0,sizeof(int)*(n+3)); //按第二关键字进行计数排序,基数排序的第一步 27 for(int i=0;i<n;i++)cntrank[rank2[i]]++; //统计排名重复的次数 28 for(int i=1;i<n;i++)cntrank[i]+=cntrank[i-1]; //统计次数累加 29 for(int i=n-1;i>=0;i--) tpsa[--cntrank[rank2[i]]]=i; //tpsa[第i个字符开头的字符串的第二关键字的名次]=i 30 memset(cntrank,0,sizeof(int)*(n+3)); 31 for(int i=0;i<n;i++)cntrank[rank1[i]]++; 32 for(int i=1;i<n;i++)cntrank[i]+=cntrank[i-1]; 33 for(int i=n-1;i>=0;i--)sa[--cntrank[rank1[tpsa[i]]]]=tpsa[i]; //sa[第二关键字排名第i的字符串的第一关键字(排名数累加--即为)排名]=第二关键字排名第i的字符串 34 rank[sa[0]]=0; 35 for(int i=1;i<n;i++) // 除第0个外,如果排名第i的字符串的第一二关键字与第i-1个的相同则排名也要相同。 36 { 37 rank[sa[i]]=rank[sa[i-1]]; 38 if(!(rank1[sa[i]]==rank1[sa[i-1]]&&rank2[sa[i]]==rank2[sa[i-1]]))rank[sa[i]]++; 39 } 40 } 41 delete []cnt;delete []cntrank;delete []rank1;delete []rank2;delete []tpsa; 42 } 43 int main() 44 { 45 scanf("%s",ss); 46 for(int i=0;ss[i];i++)s[i]=ss[i]; 47 build(s,sa,rank,strlen(ss),255); 48 for(int i=0;i<strlen(ss);i++)cout<<sa[i]<<" "; 49 cout<<endl; 50 for(int i=0;i<strlen(ss);i++)cout<<rank[i]<<" "; 51 return 0; 52 }
此外还有一个重要的工具,height数组。height[i]表示排名第i的后缀与排名第i-1的后缀的公共前缀的长度。
根据height数组的性质,求排名第i与排名第j的后缀的最长公共前缀只需要求height[i+1]到height[j]间的最小值。RMQ嘛!
height数组的求法:
1 int rank[maxn],height[maxn]; 2 void calheight(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 for(i=0;i<n;height[rank[i++]]=k) 7 for(k?k--:0,j=sa[rank[i]-1];r[i+k]==r[j+k];k++); 8 return; 9 }
注意k?k--:0的原因:
h[i]:位置为i开头的后缀与排名比它前一的后缀的公共前缀的长度。
则h[i]≥h[i-1]-1。
本人不会证明,需要证明的看博客:http://blog.csdn.net/jokes000/article/details/7839686