满心欢喜来了后缀数组,但是发现自己很懵……
后缀数组倍增算法的板子并不长,但理解起来相对很困难。
后缀的含义自然不用说了……为了简洁,我们设后缀i为从第i位开始的后缀。
需要这样几个数组:
char s[],存字符串(废话),int sa[](本体),xx[](中转数组,存rank),yy[](中转数组,存编号),c[](基排用)
然后我们需要预备知识:基数排序
具体理论就不说了,可以参考百度百科;
形象一点:找了一堆桶,把每个数/字符放在对应的桶里,然后按桶的编号挨个取出来,排好,就是基数排序。
在代码实现上c数组就是我们的桶,c[i]表示“字符值小于等于i的字符个数”
而对于字符值恰好等于i的那个字符,它的排名就是c[i](由于它是小于等于i的最大一个字符),然后我们把c[i]-=1即可
代码长这样:
1 memset(c,0,sizeof(c)); 2 for(int i=0;i<n;i++)c[x[i]=s[i]]++;//这里是一开始初始化赋值的地方 3 for(int i=1;i<m;i++)c[i]+=c[i-1]; 4 for(int i=n-1;~i;i--)sa[--c[x[i]]]=i;
接下来,我们就可以介绍本体代码了:
首先,我们对每个字符排序,这样我们会得到每个字符的排名。可能有同学有疑问,按照基排的话,相同字母的排名就不同了怎么办?后面我们还会有去重的部分,所以我们中转数组这样不重复的打也没事。在这之后,我们给每个后缀的前两个字符排序(没有第二个字符的按照0处理,0最小,以后也是这样操作)。这相当于给一些二元组排序。利用我们刚刚第一次给字母排序的结果,很容易实现这次排序(我们在从第二次开始每次排序后面都会进行一次去重)。
这之后,我们对每个后缀的前4个字符排序。由于对于后缀i和后缀j,给他们的前4个字符排序相当于“比较i和j的前两个字符”和“比较i+2和j+2的前两个字符”,而我们已经给前两个字符排好序,所以这样就可以利用前面排序的结果来操作。
不断这样操作,当每个后缀的排名都不同时,就可以跳出了。
代码见下:
1 char s[N]; 2 int sa[N],rank[N],xx[N],yy[N],c[N],n; 3 inline void get_sa(int m)//m为最大的字符值 4 { 5 int *x=xx,*y=yy; 6 memset(c,0,sizeof(c)); 7 for(int i=0;i<n;i++)c[x[i]=s[i]]++; 8 for(int i=1;i<m;i++)c[i]+=c[i-1]; 9 for(int i=n-1;~i;i--)sa[--c[x[i]]]=i; 10 for(int k=1,p=0;k<=n;k<<=1,p=0) 11 { 12 for(int i=n-k;i<n;i++)y[p++]=i; 13 for(int i=0;i<n;i++)if(sa[i]>=k)y[p++]=sa[i]-k; 14 memset(c,0,sizeof(c)); 15 for(int i=0;i<n;i++)c[x[y[i]]]++; 16 for(int i=1;i<m;i++)c[i]+=c[i-1]; 17 for(int i=n-1;~i;i--)sa[--c[x[y[i]]]]=y[i]; 18 swap(x,y); 19 p=1,x[sa[0]]=0; 20 for(int i=1;i<n;i++) 21 x[sa[i]]=(y[sa[i-1]]==y[sa[i]]&&y[sa[i-1]+k]==y[sa[i]+k])?p-1:p++; 22 if(p>=n-1)return;m=p; 23 } 24 }