后缀排序(后缀数组SA)

题目

洛谷【模板】后缀排序

省流版

对自己理解能力足够有自信的可以直接去看文章最后的代码,里面的注释本人认为讲得其实挺清楚了。

做法

很显然,这题需要把所有后缀的排名都求出来。

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;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值