zoukankan      html  css  js  c++  java
  • 【2017.10.26-】后缀数组学习笔记

    后缀数组是一个比较强大的处理字符串的算法,是有关字符串的基础算法,所以必须掌握。 
    学会后缀自动机(SAM)就不用学后缀数组(SA)了?,虽然SAM看起来更为强大和全面,但是有些SAM解决不了的问题能被SA解决,只掌握SAM是远远不够的。 

    我刚刚学习的时候是这样理解的

     

    1、构造后缀数组SA

    先定义一些变量的含义

    str :需要处理的字符串(长度为Len) 
    suffix[i] :str下标为i ~ Len的连续子串(即后缀) (这个算法在实际设计中不需要写的,这里讲解时用来表示)
    rank[i] : ruffix[i]在所有后缀中的排名 
    sa[i] : 满足rank[sa[1]] < rank[sa[2]] …… < rank[sa[Len]]的一个记录排名的数组(最前面是连续一段排名第一的子串的后缀位置,接着是连续一段排名第二的子串的后缀位置,然后以此类推,一直存到排名最后的子串的后缀位置,简单地说就是排行榜)

    。。。你不知道后缀位置是啥?。。。比如说一段下标为i ~ Len的后缀串,它的后缀位置就是第一个字符的位置,也就是i。

    前三个数组相信好理解,但sa数组如果没理解的话,可以先看看下面(我第一次也没理解sa数组是干吗的,书上没写)

     

    形象一点,出个图(这个图好像在网上横飞)
    这就是Rank和SA 
    后缀数组指的就是sa[i]。所有后缀串按照字符串顺序排序后,sa[i]表示排名第i的串的后缀位置(前面已经说了后缀位置是啥了)。

    下面介绍算法中还会提到sa数组的。

     

    有的人可能还不知道字符串序是啥。。好吧花几行说一下↓

     

    字符串比大小的方法是:将这两个相同长度的字符串从左往右遍历相同位置的字符,找到的第一位不相同的字符,哪个串在这个位置的字符小(照正常思路就是按ASC码比),哪个串就排在前面。如果遍历到了其中一个串的末尾(即遍历完了长度短的串,且长度长的串在相应前缀段的字符和它都相同),则长度短的串排在前面。

    比如abc ab两串。

    因为abc有前缀ab,ab长度短,所以ab<abc。

    再比如abc aabdef两串。

    两串公共前缀为a,第二个字符'b'>'a',所以abc>aabdef。

    注意:先遍历对照,对照到一个串的末尾时再判长度,两个步骤后别反了!

    一群字符串排序就按这个顺序分前后。

     

    回到正题。有了后缀数组,我们就可以实现一些很强大的功能(如不相同子串个数、连续重复子串等)。如何快速的到它,便成为了这个算法的关键。而sa和rank是互逆的,只要求出任意一个,另一个就可以O(Len)得到。 

    现在比较主流的算法有两种,倍增DC3,在这里,就主要讲一下稍微慢一点,但比较好实现以及理解的倍增算法(虽说慢,但也是O(Len log Len))的。

     

    倍增算法

    倍增算法的主要思想:对于一个后缀suffix[i],如果想直接得到rank比较困难,但是我们可以对每个字符开始的长度为2k的字符串求出排名,k从1开始每次递增1倍(每递增1就成为一轮),当2k大于Len时,所得到的序列就是rank,而sa也就知道了。用O(logLen)枚举k。
    这样做有什么好处呢? 
    设每一轮得到的序列为rank。有一个很美妙的性质就出现了!第k轮的rank可由第k - 1轮的rank快速得来! 
    为什么呢?为了方便描述,设Substr(i, len)为从第i个字符开始,长度为len的字符串我们可以把第k轮Substr(i, k)看成是一个由SubStr(i, k/21)Substr(i + k/21)拼起来的东西。学过倍增的人都知道,它类似rmq算法,这两个长度而2k1的字符串是上一轮遇到过的!当然上一轮的rank也知道!那么吧每个这一轮的字符串都转化为这种形式,并且大家都知道字符串的比较是从左往右,左边和右边的大小我们可以用上一轮的rank表示,那么……这不就是一些两位数(也可以视为第一关键字和第二关键字)比较大小吗!再把这些两位数重新排名就是这一轮的rank。 


    我们用下面这张经典的图理解一下: 

    就像一个两位数的比较 

    模拟一下过程,将Substr(i,2k)中的两半子串(SubStr(i, k)和Substr(i + kk))中前半段字符串记为a,后半段字符串记为b。由图可知,那个x、y数组记录的就是每次a串和b串的排名(由上次循环得来)。如果你理解了前面我提到的字符串排序的话,你应该知道在两字符串长度相等的情况下,在两串的第一个对应位置字符不相同的地方,哪个串在相应位置的字符更小,哪个串的排名就更靠前,也就是说比较字符串优先看前面部分的大小关系。然而在每次循环中k都是相等的(最外面的大循环是枚举k的,每次将k倍增),我们处理的都是长度都为k的子串。因此在这里的两串排名就是先看两串的前半段子串a的排名大小,谁的a排名靠前,谁的总排名就靠前。如果a排名相同,就看两串的后半段子串b的大小,谁的b排名靠前,谁的总排名就靠前。

     

    这里贴一下基数排序是啥:

     把数字依次按照由低位到高位(个位到高位)依次排序,排序时只看当前位。对于每一位排序时,因为上一位已经是有序的,所以这一位相等或符合大小条件时就不用交换位置,如果不符合大小条件就交换,实现可以用”桶”来做。(具体可以上网查有关资料)。

    大多数人应该知道这个原理,再看下面一段话(摘抄):

           思考一个问题:既然我们可以从最低位到最高位进行如此的分配收集,那么是否可以由最高位到最低位依次操作呢? 答案是完全可以的。

       基于两种不同的排序顺序,我们将基数排序分为LSD(Least significant digital)或MSD(Most significant digital),

       LSD的排序方式由数值的最右边(低位)开始,而MSD则相反,由数值的最左边(高位)开始。

       LSD的基数排序适用于位数少的数列,如果位数多的话,使用MSD的效率会比较好。

    由此可见,基数排序从高位到低位做完全是可以的。这样一来,两位数排序也是优先比较高位大小,再比较低位大小了,跟前面所提到的通过比较a、b给字符串排名的方法一样。实际操作中,字母最多有26个,因此前面和后面的排名的最大值是26。我们可以把它看成27进制数啊!只是用数组存两位时依然把两位数原样存进去即可(存字母反而绕弯),然后依然优先比较前面的排名,然后再比后面的排名,这样做的效果是一样的。由此可见排名的大小不会因每位不是一位数而受影响。

    综合上面的论述,我们可以知道——按照基数排序的性质,实际上对于本题中的每个子串,也可以通过先比较b的名次,再比较a的名次来确定总名次,比较次序是无关紧要的(当然你实在想先排a后排b也可以的)

    //后缀数组(suffix array)倍增构造。
    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #define maxn 100001
    using namespace std;
    char s[maxn];
    int sa[maxn],c[maxn],x[maxn*2],y[maxn*2],rank[maxn],tmp[maxn*2];//x用于表示每轮排序后的名次(从1到n),y用于临时存放某轮中给第二关键字排序后的名次 
    int read(){
        int x=0;bool f=1;char c=getchar();
        for(;!isdigit(c);c=getchar()) if(c=='-') f=0;
        for(;isdigit(c);c=getchar()) x=x*10+c-'0';
        if(!f) return 0-x;
        return x;
    }
    void build_suffix(char* s){//n表示字符串长度,m表示每次离散后的排名编号数(换句话说就是排名编号最大的是几)
        int i,p,n=strlen(s),m=0;
        memset(x,0,sizeof(x));
        //基数排序,把新的二元组排序。
        for(i=0;i<n;i++){
      	    x[i]=s[i]-'a'+1;
      	    if(!c[x[i]]) m++;//统计一开始有几个不同的字符,字符种数就是排名编号数
    	    c[x[i]]++;//c数组存储每个字符出现的次数
        }
        for(i=2;i<=m;i++) c[i]+=c[i-1];
        for(i=n-1;i>=0;i--) sa[--c[x[i]]]=i;//读者应该手算一下上图中的对应例串中,这个数组的每位是几。文章的开头说了,这就是排行榜。
     
        for(int k=1;k<=n;k<<=1){
      		p=0;
       		//基数排序二元组中的个位(即后半部分的rank值)
     		for(i=n-k;i<n;i++) y[p++]=i;//长度越界,第二关键字为0,即排在最前面
     		for(i=0;i<n;i++)  if(sa[i]>=k) y[p++]=sa[i]-k;//记录排名为i的后半段后缀所对应的前半段后缀的位置,位置由后半段起始位置-k即可得到 
      
    		//基数排序二元组中的十位(即前半部分的rank值) 
            memset(c,0,sizeof(c));
    		for(i=0;i<n;i++) c[x[y[i]]]++;
    		for(i=2;i<=m;i++) c[i]+=c[i-1];
            for(i=n-1;i>=0;i--) sa[--c[x[y[i]]]]=y[i];
      
      		//根据sa和y数组计算新的x数组 这里反过来做方便理解 
            //交换x、y数组 
      		memcpy(tmp,x,sizeof(tmp));
        	memcpy(x,y,sizeof(x));
      		memcpy(y,tmp,sizeof(y));
    
      		p=1,x[sa[0]]=1;//排第0位的排名为1,p用于存放名次 
      		for(i=1;i<n;i++)
     	    	if(y[sa[i-1]]==y[sa[i]] && y[sa[i-1]+k]==y[sa[i]+k]) x[sa[i]]=p;//特别注意y的下标+k,越界了(排名)自然就是0,但千万注意要开两倍数组以防RE 
                else x[sa[i]]=++p;
    
    		if(p>=n) break;//本轮更改后名次没改变,那么以后的倍增中名次都不会改变了。可以想一想为啥。。(想想x[i]是由什么组成的) 
    		m=p;//更新下次基数排序的最大值(当前总共有几种名次) 
        }
        
    	/*下面这段其实相当于上面最后输出的sa数组  
        for(i=0;i<n;i++) rank[x[i]]=i;//算出每个后缀的最终名次 
        for(i=1;i<=n;i++) printf("%d ",rank[i]);
        putchar('
    ');
    	*/
    }
      
    int main(){
        scanf("%s",s);
        build_suffix(s);
        return 0;
    }
    

      此贴未完,等考完noip2017后继续更

  • 相关阅读:
    CROW-5 WEB APP引擎商业计划书(HTML5方向)-微信网页版微信公众平台登录-水仙谷
    PowerShell~语法与运算符
    PowerShell和Bash的介绍
    MongoDB学习笔记~地图坐标的支持与附近点的查找
    Linux~Sh脚本一点自己的总结
    我在百度开放云编程马拉松上的一个创意
    JavaScript字符串插入、删除、替换函数
    社会化登录之豆瓣小结
    C#在64位操作系统上连接Oracle的问题和解决方案
    asp.net(C#)链接Oracle连接字符串
  • 原文地址:https://www.cnblogs.com/scx2015noip-as-php/p/suffixarray.html
Copyright © 2011-2022 走看看