zoukankan      html  css  js  c++  java
  • 后缀数组之倍增算法

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

  • 相关阅读:
    面向对象第三单元博客作业
    面向对象编程第2次总结(电梯作业)
    面向对象编程第1次总结
    OOP 第四章博客总结
    OO 第三章总结
    OOP 第二章作业总结
    Java 设计模式 -- 代理模式
    ASID 与 MIPS 中 TLB 相关
    Java 锁机制总结
    OOP 第一章作业总结
  • 原文地址:https://www.cnblogs.com/gryzy/p/6424564.html
Copyright © 2011-2022 走看看