后缀数组是一个比较强大的处理字符串的算法,是有关字符串的基础算法,所以必须掌握。
学会后缀自动机(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数组是干吗的,书上没写)
形象一点,出个图(这个图好像在网上横飞)
后缀数组指的就是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/2−1)和Substr(i + k/2, k - 1)拼起来的东西。学过倍增的人都知道,它类似rmq算法,这两个长度而2k−1的字符串是上一轮遇到过的!当然上一轮的rank也知道!那么吧每个这一轮的字符串都转化为这种形式,并且大家都知道字符串的比较是从左往右,左边和右边的大小我们可以用上一轮的rank表示,那么……这不就是一些两位数(也可以视为第一关键字和第二关键字)比较大小吗!再把这些两位数重新排名就是这一轮的rank。
我们用下面这张经典的图理解一下:
模拟一下过程,将Substr(i,2k)中的两半子串(SubStr(i, k)和Substr(i + k, k))中前半段字符串记为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后继续更