文章目录
后缀数组是真的难,我花了一个月总算有了一点比较基础的概念,然后就马上写学习笔记了QMQ。
普通后缀数组
导语
暴力基排?不不不, O ( n 2 ) O(n^2) O(n2)时间,太慢了!后缀数组就是专门解决这个问题哒。
BB几句
后缀数组我分两种构建算法
- 倍增算法
- SA-IS
其实原本只打算学SA-IS,毕竟 O ( n ) O(n) O(n),常数还比DC3要小,就是思想复杂了点QAQ。
于是就跟机房里一开始学倍增的一位大佬争了个“你死我活”,结果后来又发现树上后缀数组只能用倍增,QAQ,真香。
倍增算法
过程
先讲倍增吧,因为实在不想提前体验SA-IS博客的长度。
倍增,顾名思义,1,2,4,8…,后一个是前一个的两倍,那么,后缀数组跟倍增有什么关系?
首先,对所有的字母基数排序一遍。
温馨提示:倍增用基排 O ( n l o g n ) O(nlogn) O(nlogn),用快排 O ( n l o g 2 n ) O(nlog^2n) O(nlog2n)
但是这里面有三个2,两个1,一个3,那么我们该怎么让他们彼此不相等呢?
回想普通暴力基排时,排序完了第一个字母,就排序第二个字母,那么,倍增也一样,将每个字母的后面一个字母的排名合并到这个字母上,没有字母就当后面字母的排名为0。
合并方式见图片:
那么我们只要对合并后的两位基数排序就行了,正确性显而易见,就是排序每个字母开始的两个字符。
但是还是有两个4,那么我们是不是继续基排第三个字符?不行,不然跟暴力差不了多少,那怎么做?
回想一下,我们已经排序了每个后缀的前两个字符,那么每个后缀的第三位与第四位是不是就是这个后缀的下下个后缀的第一位与第二位?
那么,每个后缀只需要再把他后两个的后缀的排名合并起来,基排一遍。
至此,没有一个相似的,就结束。
总结
- 过程总结
倍增的过程总结一下,首先排序每个后缀第一个字符。
然后合并前2个,基排,再合并前4个,基排,再合并前8个,基排…
- 一小点小东西。
我们设现在合并的是每个后缀前 k k k个字符( k k k为2的倍数),那么,那么 n − k + 1 n-k+1 n−k+1到 n n n个后缀的排名的个位都是0,同时十位互不相同。
这个很好想,因为倍增原本就是正确的QMQ。
好吧,我想是因为(前方高能,仅为作者临时所想):
那么 n − k + 1 n-k+1 n−k+1到 n n n个后缀的排名的个位都是0,这个不讲,So easy。
证明后一句话:
我们设x=k/2,也就是上一步过程。
假设再上一步过程之前, n − k + 1 n-k+1 n−k+1到 n − x n-x n−x都有相同的话,那么不同只能不同在个位,也就是他们分别合并的 n − x + 1 n-x+1 n−x+1到 n n n,那么,一步步下去,最终 x x x会折中到 0 0 0, k k k到 1 1 1,那么这是 n − k + 1 n-k+1 n−k+1到 n − x n-x n−x只有一个数,肯定不相等。
同时 n − k + 1 n-k+1 n−k+1到 n − x n-x n−x的排名肯定没有一个与 n − x + 1 n-x+1 n−x+1到 n n n相等,因为再合并每个后缀前x个的时候, n − x + 1 n-x+1 n−x+1到 n n n的排名的个位都是0,而 n − k + 1 n-k+1 n−k+1到 n − x n-x n−x的排名的个位都是大于0的数,因此 n − k + 1 n-k+1 n−k+1到 n n n个后缀的排名互不相等。
那么,上一句话是对的,我们就可以进而推导出倍增的过程到 l o g 2 ( n ) + 1 log_{2}(n)+1 log2(n)+1是肯定每个后缀排名互不相等,故倍增 O ( n l o g n ) O(nlogn) O(nlogn)。
代码
#include<cstdio>
#include<cstring>
#include<algorithm>
#define N 1200000
using namespace std;
int xx[N],yy[N],cc[N],sa[N],n,m;
char st[N];
void get_sa()
{
//基数排序
for(int i=1;i<=n;i++)cc[xx[i]=st[i]]++;//统计每个字符有多少个
for(int i=2;i<=m;i++)cc[i]+=cc[i-1];
for(int i=n;i>=1;i--/*仅对一个字符排序,正的反的都可以*/)sa[cc[xx[i]]--]=i;//计算第一个字符
//先对第一个字符排序
//cc
for(int k=1;k<=n;k<<=1)
{
//yy数组相当于对个位排序的每个后缀的排名。
int num=0;
for(int i=n-k+1;i<=n;i++)yy[++num]=i;//为0的先预先都到yy数组里面去
for(int i=1;i<=n;i++)//遍历一边sa数组
{
if(sa[i]>k)yy[++num]=sa[i]-k;//将自己的值给到-k的位置上
}
//yy
memset(cc+1,0,(m<<2));//部分清0,int有4个字节
//对十位排序
for(int i=1;i<=n;i++)cc[xx[i]]++;//统计
for(int i=2;i<=m;i++)cc[i]+=cc[i-1];//统计
for(int i=n;i>=1;i--/*按照yy数组从大到小*/)sa[cc[xx[yy[i]]]--]=yy[i];//按照十位一个个对到sa数组里面去
//cc
swap(xx,yy);//将xx数组给到yy数组,同时xx数组要更新出新的xx数组,相当于原来的yy数组已经没用了。
xx[sa[1]]=num=1;//先处理第一项
for(int i=2;i<=n;i++)
{
xx[sa[i]]=(yy[sa[i]]==yy[sa[i-1]]/*原本的排名相同*/ && yy[sa[i]+k]==yy[sa[i-1]+k]/*加k位的排名相同*/)?num/*不变*/:++num/*+1*/;
}
if(n==num)break;//优化
m=num;//更新m
}
}
int main()
{
scanf("%s",st+1);
n=strlen(st+1);m=122;
get_sa();
for(int i=1;i<n;i++)printf("%d ",sa[i]);//输出
printf("%d\n",sa[n]);
return 0;
}
注意:这里的基数排序写法可能有点奇怪,但是很实用,其实就是借用了桶思想的基数排序。
玄学优化
听说把 x x [ y y [ i ] ] xx[yy[i]] xx[yy[i]]用另一个数组储存起来,能加速到很快的时间。
SA-IS
一个写起来会死人的算法,QAQ。
一个写得十分好的博客,比较建议去这里看,毕竟我的语文不是很好
大佬:SA-IS核心就是诱导排序。
一步步慢慢讲,不急。。。QAQ
过程
导语
首先,我们定义type数组,如何灵活应用这个数组是这个算法的核心。
定义s数组代表字符串,一个后缀的集合表示为字母大写,第 i i i个后缀代表以第 i i i个字母开头的后缀。
所有下标从1开始!!!
在刚开始的时候,我们要给s数组的最后加一个比所有字符都要小的字符,假设就是’#’。
type与LMS的构建
type数组分三种情况(默认最后一位,也就是’#‘为’S’):
- 当 s [ i ] < s [ i + 1 ] s[i]<s[i+1] s[i]<s[i+1]时, t y p e [ i ] = ′ S ′ type[i]='S' type[i]=′S′。
- 当 s [ i ] > s [ i + 1 ] s[i]>s[i+1] s[i]>s[i+1]时, t y p e [ i ] = ′ L ′ type[i]='L' type[i]=′L′。
- 当 s [ i ] = s [ i + 1 ] s[i]=s[i+1] s[i]=s[i+1]时, t y p e [ i ] = t y p e [ i + 1 ] type[i]=type[i+1] type[i]=type[i+1]。
注意,在后文中提到的一个后缀的类型就是这个后缀首字母的 t y p e type type。
那么我们倒着搜 O ( n ) O(n) O(n)可以处理出来
//0为S,1为L。
//因为type数组默认清零,所以type[n]=0
for(int i=n-1;i>=1;i--)
{
if(st[i]>st[i+1])type[i]=1;
// if(st[i]<st[i+1])type[i]=0;默认为0,所以可以不写
else if(st[i]==st[i+1])type[i]=type[i+1];
}
//type
那么 L M S LMS LMS是什么情况?就是当 t y p e [ i ] = ′ S ′ type[i]='S' type[i]=′S′ && t y p e [ i − 1 ] = ′ L ′ type[i-1]='L' type[i−1]=′L′时,我们称 i i i位 L M S LMS LMS节点。
同时称两个 L M S LMS LMS节点间(包括两个点)的子串叫LMS子串,以LMS节点为开头的后缀叫LMS后缀。
定理1
'#'一定是 L M S LMS LMS节点
显而易见,’#'比所有节点小,自然比 s [ n − 1 ] s[n-1] s[n−1]小,同时他还默认十 S S S类型
定理2
那么我们可以发现 L M S LMS LMS节点就是一大串 L L L后面的第一个 S S S,因此我们可以知道 L M S 子 串 LMS子串 LMS子串肯定是前面一段 S S S类型,中间一段 L L L类型,最后一个 S S S类型的。
给个表格帮助理解:
下标 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|
字母 | a | b | c | a | a | c | d | e | # |
type | L | L | L | S | S | S | S | L | S |
L M S LMS LMS | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 |
定理3
对于两个后缀 A A A与 B B B,当 A [ 1 ] = B [ 1 ] A[1]=B[1] A[1]=B[1]且A的类型为 L L L, B B B的类型为 S S S,则 A < B A<B A<B
证明:
设 A = a b X , B = a c Y A=abX,B=acY A=abX,B=acY
我们可以知道 b ≤ a , c ≥ a b≤a,c≥a b≤a,c≥a,当 b < a b<a b<a或 c > a c>a c>a时,很明显我们要讨论 a = b , a = c a=b,a=c a=b,a=c的情况。
当 a = b , a = c a=b,a=c a=b,a=c时,我们转化为讨论 a X aX aX与 a Y aY aY的关系,不断下去。
由于 t y p e [ n ] = S , t y p e [ n − 1 ] = L type[n]=S,type[n-1]=L type[n]=S,type[n−1]=L,所以A顶多转化到第 n − 2 n-2 n−2个后缀。而B顶多转化到第 n − 1 n-1 n−1个后缀,且都最少有两个字符,因此我们可以不用担心没有 b b b或 c c c的情况。
这个有什么用?
也就是说在sa数组里面,同样首字母的后缀,类型是 L L L排前面,类型是 S S S的排后面
诱导排序的基本过程
诱导排序先讲过程再讲证明会简单一点。
其实type数组与LMS数组就是诱导排序的核心数组,那么如何排序呢?
首先,我们假设LMS后缀是之间已经排好序的了。
过程:
- 用桶排序思想将每个字符的出现次数记录,同时用 l b s a lbsa lbsa与 r b s a rbsa rbsa数组记录每个字符在 s a sa sa数组中出现的左端点与右端点。
- 逆序扫描LMS数组,同时将LMS后缀依次加入S型桶里面。
- 正序扫描一遍sa数组,当 t y p e [ s a [ i ] − 1 ] = L type[sa[i]-1]=L t