后缀数组之倍增算法

Posted gryzy

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了后缀数组之倍增算法相关的知识,希望对你有一定的参考价值。

首先说明 :后缀数组的构建在网上有多种方法:朴素的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 }
View Code

  基数排序,也称桶子排序(注意和上面的区分),实际上是分关键安排序。首先按次关键字排,再按首关键字排。

 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 }
View Code

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 }
View Code

 此外还有一个重要的工具,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 }
View Code

注意k?k--:0的原因:

h[i]:位置为i开头的后缀与排名比它前一的后缀的公共前缀的长度。

h[i]≥h[i-1]-1。

本人不会证明,需要证明的看博客:http://blog.csdn.net/jokes000/article/details/7839686

以上是关于后缀数组之倍增算法的主要内容,如果未能解决你的问题,请参考以下文章

●后缀数组○十三个例题

后缀数组-倍增算法模板

P3809 后缀排序(倍增法后缀数组)

P3809 后缀排序(倍增法后缀数组)

后缀数组

后缀排序