题目
省流版
对自己理解能力足够有自信的可以直接去看文章最后的代码,里面的注释本人认为讲得其实挺清楚了。
做法
很显然,这题需要把所有后缀的排名都求出来。
O(n²㏒2n)
这个非常容易想到,只需要把所有的后缀都进行比较并记录下它们各自的排名即可。
其中比较的时间复杂度为O(n),每次排序的时间复杂度为O(n㏒2n)。
代码如下:
#include<bits/stdc++.h>
using namespace std;
const int N=1e7;
int n,p[N+5];
char c[N];
bool cmp(int a,int b){
//使用strcmp函数会比手写快,且手写容易爆空间
return strcmp(c+a,c+b)<0;
}
int main(){
cin>>(c+1);
n=strlen(c+1);
for(int i=1;i<=n;i++) p[i]=i;
sort(p+1,p+1+n,cmp);
for(int i=1;i<=n;i++) printf("%d ",p[i]);
return 0;
}
O(n㏒²2n)
有这么一个非常好用的方法,叫作倍增法,求解RMQ方法中的st表以及用欧拉序求树上两个点的LCA运用的正是这个方法,求后缀排序时也可以运用这一方法来优化时间。
首先,能使用倍增法的根本原因是字符串间的比较方法——从第一位开始比较,相同则往后继续比较,不同则哪一个字符串这一位的ASCII码(该位为空则ASCII码为0)更大,其字典序更大。
根据字符串间的比较方法,就可以发现最朴素的比较方法是从前往后一个个比较,而这可以用倍增法进行优化,从而将比较的时间复杂度从O(n)降到O(㏒2n)。
至于怎么优化呢?很简单,先从每个后缀的长度为1的前缀开始一一比较并排序,然后开始倍增地比较,例如:
此时倍增的跨度为 k = 2i(k ≤ n,n 为字符串长度),比较对象为后缀 x 和后缀 y ,上一轮排序后每个后缀暂时的排名为数组a,则先比较 a[x] 与 a[y] ,二者相同则比较 a[x+k] 和 a[y+k] ,仍相同则代表二者的长度为(k*2)的前缀相同,x和y的排名也暂时相同。
值得一提的是,在倍增比较的过程中,若每个后缀的用于比较的前缀都不相同,则它们前缀的排名就是它们的排名。
注:因为此优化在下个方法仍被使用,故针对本算法不提供代码。
O(n㏒2n)
目前,比较已经使用了倍增法优化,观察下可以发现,比较的时候只有两个关键字,故而排序也可以进行一定的优化——使用基数排序将时间复杂度从O(n㏒2n)降到O(n)。
代码如下:
#include <bits/stdc++.h>
/*
时间复杂度:O(n㏒n)
sa[i]:所有后缀排序后第i小的后缀编号
rk[i]:后缀i的排名
*/
using namespace std;
const int N=1e6+5;
int n,sa[N],rk[N<<1],cnt[N],x[N];//在开O2优化时,在使用memcpy的时候,两个数组的大小必须相同,不然会RE
char s[N];
int main(){
scanf("%s",s+1);
n=strlen(s+1);//字符串长度
int m=127;//排序后的最后一个排名,初始为127是因为ASCII码的最后一位为127
//当长度为1时,进行基数排序(https://oi-wiki.org/basic/radix-sort/)
for(int i=1;i<=n;i++) cnt[rk[i]=s[i]]++; //记录每个排名的数量
for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
for(int i=n;i>0;i--) sa[cnt[rk[i]]--]=i; //更新sa数组
for(int k=1;k<=n;k<<=1){//倍增,k表示长度/2
//关键性越低的关键字越先对排序数组造成贡献
//第二关键字排序
memset(cnt,0,sizeof(cnt));
memcpy(x,sa,sizeof(x));//将sa复制给x
for(int i=1;i<=n;i++) cnt[rk[x[i]+k]]++;//记录下每个长度为k*2的子串中,其后半部分的每个子串(包括空子串)对应的排名的数量
for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];//上一轮排序的排名最后一位为m,故只需遍历到m即可;cnt[0]代表空子串
for(int i=n;i>0;i--) sa[cnt[rk[x[i]+k]]--]=x[i];//更新sa数组
//第一关键字排序
memset(cnt,0,sizeof(cnt));
memcpy(x,sa,sizeof(x));//将sa复制给x
for(int i=1;i<=n;i++) cnt[rk[x[i]]]++;//记录下每个长度为k*2的子串中,其前半部分的每个子串对应的排名的数量
for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
for(int i=n;i>0;i--) sa[cnt[rk[x[i]]]--]=x[i];//更新sa数组,此时的sa数组已经排好序
memcpy(x,rk,sizeof(x));//将rk复制给x
m=0;//因为要重新计算排名总数,故m需要清零
for(int i=1;i<=n;i++){
if(x[sa[i]]==x[sa[i-1]]&&x[sa[i]+k]==x[sa[i-1]+k]){//在sa数组中,第i小的子串与第i-1小的子串相同,则二者的排名是一致的,均为m
rk[sa[i]]=m;
}
else rk[sa[i]]=++m;
}
if(m==n) break;//每个后缀的前缀均不相同,则每个后缀的前缀的排名也是每个后缀的排名
}
for(int i=1;i<=n;i++) printf("%d ",sa[i]);
return 0;
}