字符串系列(二)——“万金油”后缀数组

学习后缀数组有感

      (原创,转载请注明出处)

        在做一些串的问题时,因为其本身处理比较麻烦,光是比较就要耗费O(n²)的复杂度。因此我们使用后缀数组来进行复杂度的简化。

        首先要明确,后缀数组是一种工具,可以帮我们得到一些数组。而后期要根据我们自身对于这些数组含义、性质的理解,去运用它们解出题目。

        先来看一个问题:现给定一字符串长度为n,把它的后缀们按ascii序排序后,每一个后缀的排名,我们记为rank[]。以及它的反数组(可参照反函数)sa[]。这里易得sa[rank[i]] = i以及rank[sa[i]] = i。

        在涉猎后缀数组前,我们会想到使用qsort进行排序,然后取值。这里快排效率为O(),看似效率快,但要知道,字符串的快排要挨位去比较,要在快排上乘一个n,即为O()。

        虽然,我们有字符串排序中效率较高的Multi-key Quick Sort,然而最坏的情况依旧会达到O(n²)。

        这样的效率显然是我们无法忍受的。所以,有后缀数组。

        为什么我们在别的排序中不使用后缀数组呢?初学者可能会问这种比较水的问题。但是话说回来,这个问题也点出后缀数组的关键点——利用同一字符串后缀的性质即位置相邻后缀suffix[p]去掉第一个字母后与suffix[p + 1]两个串全等。

        构造后缀数组,我们要掌握一种倍增算法。有一种基数排序(bin sort)可以对于字符串较高效进行处理。原因是字符串的大小是优先判断首位,azzzzzzzzzzzz和baaaaaaaaaa两个字符串相比依旧是前者位置靠前。基数排序时我们就可以从首位开始做。

        然而,这种效率依旧较低,而考虑到我们前面所说的那个关键点,我们再提取基数排序第二关键字关键字时可以直接由前面的第一关键字得到,十分方便。

        这里我们是不需要基数排序的,只需要用到其倍增思想。不难想象,倍增效率最多为O(),而一共做n次即可,二者相乘把复杂度成功降到了O(n)。

        后缀数组的初始关键字从哪来呢?网上的许多讲解都是通过先给所有的后缀的第一位进行排序的,较赘余。这里有一种部分和的方法可以轻松解决。

memset(st, 0, sizeof(st));//st[]存每个字母是否出现
for(int i = 1; i <= n;++i) st[a[i]] = 1; //只存1或0,避免重名时的复杂取舍
for(int i = 1; i <=255; ++i) st[i]+= st[i - 1]; //直接得出字母的名次,与离散化异曲同工
memset(rank, 0,sizeof(rank));
for(int i = 1; i <= n;++i) rank[i] = st[a[i]]; //每个后缀首字母的排序

       既然,我们已经解决这个问题,那么直接进行求解就好了。

   int k = 0;
   //p是向下推p个单位即可得到此时的第二关键字
   //k是指当前最大名次,若等于n了则已经全部排序完毕没有相同名次,可以退出循环
   for(int p = 1; k != n; p+=p) {
           //1、对第一关键字排序
           memset(cnt, 0, sizeof(cnt));
           for(int i = 1; i <= n; ++i) ++cnt[rank[i + p]]; //向柱子上加入元素(下同)
           for(int i = 1; i <= n; ++i) cnt[i]+= cnt[i - 1]; //部分和(下同)
           for(int i = n; i >= 1; --i) tmp[cnt[rank[i + p]]--] = i;//第一关键字排序结果(下同)
           //2、对第二关键字排序
           memset(cnt, 0, sizeof(cnt));
           for(int i = 1; i <= n; ++i) ++cnt[rank[i]];
           for(int i = 1; i <= n; ++i) cnt[i]+= cnt[i - 1];
           for(int i = n; i >= 1; --i) sa[cnt[rank[tmp[i]]]--] =tmp[i];
           //3、通过sa[]确定rank[]
           memcpy(rank1, rank, sizeof(rank1)); //或者写“memcpy(rank1, rank,sizeof(rank/2));”
           rank[sa[1]] = 1; k = 1; //第一名一定当前排名为1
           for(int i = 2; i <= n; ++i) {
                   if(rank1[sa[i]]!= rank1[sa[i - 1]] || rank1[sa[i] + p] != rank1[sa[i - 1] + p]) ++k; //若是二者不重复那么一定编号向后加一个;若是二者完全相同则编号也应相同
                   rank[sa[i]] = k;
           }
   }

        现在我们得到了两样工具,即rank[]与sa[]。而我们还差了一个很重要的数组,height[]。

        现给出定义:

        height[i]:表示相邻排名的两个后缀(这里定义为第i名与第i-1名)的最长LCP(公共前缀)的长度。

        首先要进行特判,height[1]必定为0。

        为了求其他的height,我们定义一h[]使得height[rank[i]] = h[i],即表示第i个后缀与此后缀的前一名的后缀的the length of maximum LCP。

        那么现在重点来了。【敲黑板】为了简化我们搜索的效率,我们每次搜索前缀不必从头开始,可以利用某些结论。

        h[i + 1] >= h[i] - 1

        利用这个结论可轻松得出height。

k = 0;
for(int i = 1; i <= n;++i) {
    if(rank[i] == 1) {height[1] = 0; k = 0; continue;}
    k--; if(k < 0) k = 0;
    while(a[i + k] == a[sa[rank[i] - 1] + k]) ++k;
    height[rank[i]] = k;
}

        至此,后缀数组这个工具我们已经做好了。

        注意:

                  ①rank数组的定义:我们最多要用到rank[n+ p],所以保险起见开到2*N。但是我个人习惯开1.1倍。p的取值范围是倍增得到的,最多不会超过,而0.1N足以应付。写的时候直接开一倍就好。

                  ②后缀数组最好要从1开始做下标。网络上好多用0的,但是这样的话在rank与串之间就要细细思考。

                  ③勤练习bin sort十分有益。

 

 

 

 

        既然现在有了这把刀,那怎么去使用呢?

        举一些有趣的例子。比如:

        ①回文

        处理回文类问题,有好多方法,比如回文自动机(自学了一下发现和AC自动机挺像的),还有manacher算法(O(n)回文子串)等等等。

        但是我们知道,字符串问题中,后缀数组大多数时候都是万金油一样的存在。

        对于此类问题我们只需要把这个给定长串再次反接在串后,中间一定要加分隔符。(原因:如若是不加的话回导致中间连在一起的部分出现问题,导致答案错误)

        ②最长重复子串:处理时在后面原序加一遍,中间还是加分隔符即可。

       

接下来我们说一说后缀数组在实际科技发展中的用途。(其实吧可以用这个解决一下tjoi2017的DNA)

依然举个例子。众所周知,组成生物体DNA的碱基有四种——AGCT。随着近几年生物基因工程的发展,随之而来的,我们需要得到一种对长字符串(即DNA序列)快速操作的手段。这时,后缀数组因为它速度快、功能强大的特点脱颖而出。这时可能有人会问,KMP也可以做到啊。然而KMP所追求的的是一种精确的匹配,解释一下,就是零容错。实际生活中,即使母子的DNA也不可能完全相同啊。那使用KMP,得到的结果就永远都是匹配失败咯。相反,后缀数组可以完美的解决这个问题。大致的做法是:由于我们可以先用nlogn求出所有lcp,然后查询就只需要O(1)了。于是枚举原串每一个字符作为起点,如果相同就用lcp求,不同就给计数器加1。最后计数器如果不超过某个值,我们就统计答案。

再举个例子。在网页文本的特征抽取过程中(可以理解为,get网页的大体内容),重复的短语识别是一个十分关键的技术。在我们识别之后,得到的东西会更加具有代表性。对于单一长串,后缀数组当然是最佳的选择啦。

 


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值