前言:
后缀数组 ($Suffix\ Arrar$),是一种对一个字符串的所有后缀进行字典序排序的算法
据说有几种 $O(n)$ 复杂度的写法,但本蒟蒻都不会......
所以这里就介绍一种 $O(nlogn)$ 的倍增做法
正文:
后缀排序
关于后缀排序的过程演示,我实在是懒得画图了
所以安利一下这个博客 后缀数组 最详细(maybe)讲解
关于它的复杂度,首先倍增的复杂的是 $O(logn)$ 的
而正常 $sort$ 一次的复杂度是 $O(nlogn)$ 的
这样后缀数组的复杂度就会退化为 $O(nlog^2n)$
所以我们考虑优化它的排序方式
由于每次都是对一个二元组进行排序
所以这里引入一种新的排序方式叫基数排序
它能将这个二元组以 $O(n)$ 的复杂度进行排序
这样就可以满足我们对后缀数组 $O(nlogn)$ 复杂度要求了
关于代码,按我们 $pc$ 大佬的话,简直和狗屎一样
我足足理解了 3 天,希望能把它解释明白
int n,m;//n表示字符长度,m表示字符集大小 char s[maxn]; int sa[maxn],tp[maxn];//sa[i]表示排名为i的后缀的起始位置的下标,tp[i]表示第二关键字排名的i的起始位置的下标 int rank[maxn],tong[maxn];//rank[i]表示以i为起始位置的后缀的排名,tong数组为基数排序要用到的tong void qsort() { for(int i=0;i<=m;i++) tong[i]=0;//将tong数组清零 for(int i=1;i<=n;i++) tong[rank[i]]++;//将第一关键字基数排序 for(int i=1;i<=m;i++) tong[i]+=tong[i-1];//求前缀和 for(int i=n;i>=1;i--)//用前缀和求排名,所以要倒着循环,觉得不好理解的可以手模一下 sa[tong[rank[tp[i]]]--]=tp[i];
//tong[rank[tp[i]]]表示第二关键字排名为i的起始位置的第一关键字排名,自减后为下一相同第一关键字的排名 } void get_sa() { m=127;//ASCII码最大值 for(int i=1;i<=n;i++) { rank[i]=s[i];//初始第一关键字赋为它的ASCII码值 tp[i]=i;//第二关键字初始化为本身 } qsort();//基数排序 for(int w=1,p=1;p<n;m=p,w<<=1)//倍增,m=p算一个小优化 { p=0; for(int i=1;i<=w;i++) tp[++p]=n-w+i;//最后w个字符第二关键字为0,所以排名在最前面 for(int i=1;i<=n;i++) if(sa[i]>w)//第一关键字排名为i的起始位置为它前w位的第二关键字 tp[++p]=sa[i]-w; qsort();//将第一关键字排序 std::swap(rank,tp);//这里节约空间,由于tp数组已经没有用,所以用它存储上一次的rank rank[sa[1]]=p=1;//重新排名 for(int i=2;i<=n;i++)//如果第一关键字和第二关键字都相同,则排名相同 rank[sa[i]]=(tp[sa[i-1]]==tp[sa[i]]&&tp[sa[i-1]+w]==tp[sa[i]+w])?p:++p; //如果p==n,即每一个后缀排名都不一样,则已排好所有后缀,跳出循环 } }
如果对 $sa$ 数组和 $tp$ 数组理解起来比较困难
引用 $pc$ 的另一句话,$sa$ 数组和 $tp$ 数组的本质其实是字符串
它储存的是每个后缀的起始位置的下标,这样也许能帮你更好的理解这两个数组
应用
完成了后缀排序之后,我们考虑后缀数组的应用
因为对于一个字符串,它所有后缀的所有前缀就是它的所有子串
所以我们要引入 $height$ 数组,它能把各各后缀之间的关系联系起来
$height [i]$ 表示后缀排序后排名为 $i$ 的后缀和排名为 $i-1$ 的后缀的最长公共前缀($lcp$)
按本宇大佬的话,有一个非常好证明的性质是,$height [rank [i]] \geq height [rank [i]-1]-1$
所以我们就根据这个性质来求这个 $height$ 数组,$pc$ 说不会证就直接记住吧,所以我很听话
int height[maxn]; void get_he() { int k=0; for(int i=1;i<=n;i++) rank[sa[i]]=i;//求出每个后缀的排名 for(int i=1;i<=n;i++) { if(rank[i]==1) continue;//排名为1的后缀的height值为0 if(k) k--; int j=sa[rank[i]-1];//根据上文提到的性质 while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k]) k++; height[rank[i]]=k; } }
求出 $height$ 数组后就可以搞事情了
比如可以求任意两个后缀(起始位置下标分别为 $a$ 和 $b$ )的 $lcp$
很显然答案是 $[a+1,b]$ 区间 $height$ 的最小值(这里保证 $a<b$ )
所以我们可以用 $st$ 表来维护一下区间最小值
int lg[maxn]; int st[maxn][22]; void build_st() { lg[1]=0; for(int i=2;i<=n;i++)//递推出log值 lg[i]=lg[i>>1]+1; for(int i=1;i<=n;i++)//初始化 st[i][0]=height[i]; for(int j=1;j<=lg[n];j++)//注意先枚举j for(int i=1;(i+(1<<j)-1)<=n;i++) st[i][j]=std::min(st[i][j-1],st[i+(1<<(j-1))][j-1]); }
建好 $st$ 表后,就可以 $O(1)$ 查询 $lcp$
int get_lcp(int a,int b) { a=rank[a],b=rank[b];//得到两个后缀的排名 if(a>b) std::swap(a,b);//保证a<b a++;//[a+1,b]区间,所以a要加一 int k=lg[b-a+1]; return std::min(st[a][k],st[b-(1<<k)+1][k]);//st表查询区间最值 }
此外,$height$ 数组还经常和二分答案联系在一起
对于 $check$ 函数,我们一般把 $height<mid$ 的边断开
这样我们相当于将这些后缀分好了组,常对一组内后缀进行统计,来判断 $mid$ 值是否可行
比如这个题要求出现次数达到 $k$ 次的最长子串(题目来自落谷P2852)
bool check(int mid) { int sum=1; for(int i=2;i<=n;i++) { if(height[i]<mid) sum=1; else sum++; if(sum>=k) return 1; } return 0; }
后序:
个人感觉后缀数组比后缀自动机好用
虽然我并不会用后缀数组,也不会也写后缀自动机......