西安多校集训-后缀数组

后缀

定义字符串 S 。
后缀就是以 \(i\) 位置为起始到 S 末尾的子串。
比如令 S=abcab
则以 \(2\) 起始的后缀为 bcab。

后缀数组

\(sa_i\) 表示排名为 \(i\) 的后缀的起始位置的下标。
仍令 S=abcab
那么 S 的后缀分别有:
abcab(1)
bcab(2)
cab(3)
ab(4)
b(5)
对其按字典序排序,轻松得到:
ab(4)
abcab(1)
b(5)
bcab(2)
cab(3)
如果让我们输出 sa 数组,就是例题:

【模板】后缀排序

最朴素的想法是什么?
因为字典序的排序关键字是最靠左的字符,所以我们可以依次枚举所有后缀,然后挨个排序。
复杂度是 \(O(n^2\log n)\) ,肯定是不能接受的。
所以我们使用稍稍高级的方法:

倍增

仔细拜读题解后,感觉题解没说人话。所以我来说我能听懂的人话(当然我也可能不是人)
仍令S=abcab
在读入时我们便先做一次排序,但这次排序只对单个字符 \(i\) 排序,允许并列,比如第一次排序后,得到的序列如下:
1 2 3 1 2
随后两两进行合并,位数不够用0补全(空字符字典序最小),得到
12 23 31 12 20
排序,得到
1 3 4 1 2
此时序列中第一个 1 表示的就是以 1 为起始位置,长度为 2 的子串在所有长度为 2 的子串中的排名。
序列中的 4 表示的就是 以 3 为起始位置,长度为 2 的子串在所有长度为 2 的子串中的排名。
我们发现,如果我们令 1,4 接到一起,是否可以得到以 1 为起始位置,长度为 4 的子串在所有长度为 4 的子串中的排名呢?
答案是可以的。感性理解的话就是因为决定排名的关键字是最靠左的字符,所以我们这么合并仍保证了最靠左字符大的不会变的比最靠左字符小的小。(是否可以理解为拼后缀时整个子串的字典序单调不降呢?我想是可以的。)
那么后续思路不就有了,依次合并新序列的 \(i\)\(i+2^k\) 即可。
都手模到这了,直接模完吧。
1 3 4 1 2
接下来合并 \(i\)\(i+2\) ,位数不够补0,得到
14 31 42 10 20
排序得到
2 4 5 1 3
然后依据 SA 数组的定义:
1 2 3 4 5
2 4 5 1 3
得到最后的 SA 数组:
4 1 5 2 3
好了接下来就是代码了,其他的详见代码:

#include<iostream>
#include<cstring>
using namespace std;
const int N=1e6+10;
int sa[N],x[N],y[N];
//sa 数组同上,x 数组表示 i 位置的字母大小,y 表示 i+2^k 位置的字母大小
int cnt[N],num;
//如果使用快排,复杂度是双log 的,但是因为我们每次排序两个值,所以使用基数排序,复杂度变为 单log。
int m,len;
string s;
void SA(){
	for(int i=1;i<=len;i++){
		x[i]=s[i];
		cnt[x[i]]++;
	}
//  准备第一次基数排序,将 i 位置的字符入桶
	for(int i=2;i<=m;i++){
		cnt[i]+=cnt[i-1];
	}
//  做前缀和可知相同字符最大可排在多少
	for(int i=len;i>=1;i--){
		sa[cnt[x[i]]--]=i;
	}
//  基数排序
//  下面是合并 i 与 i+2^k 的过程
	for(int k=1;k<=len;k<<=1){
		int num=0;
		for(int i=len-k+1;i<=len;i++){
			y[++num]=i;
		}
//    因为此时会出现位数不够的情况,所以提前处理。
		for(int i=1;i<=len;i++){
			if(sa[i]>k){
				y[++num]=sa[i]-k;
			}
		}
//    i+2^k 位置的字符入桶
		for(int i=1;i<=m;i++) cnt[i]=0;
		for(int i=1;i<=len;i++) cnt[x[i]]++;
		for(int i=2;i<=m;i++) cnt[i]+=cnt[i-1];
		for(int i=len;i>=1;i--){
			sa[cnt[x[y[i]]]--]=y[i];
			y[i]=0;
		}
//    基数排序
		swap(x,y);
		x[sa[1]]=1,num=1;
		for(int i=2;i<=len;i++)
			x[sa[i]]=(y[sa[i]]==y[sa[i-1]] && y[sa[i]+k]==y[sa[i-1]+k]) ? num : ++num;
		if(num==len) break;
		m=num;
//    更新 i 位置的子串情况
	}
	for(int i=1;i<=len;i++) cout<<sa[i]<<' ';
}
int main(){
	cin>>s;
	len=s.length();
	s=' '+s;
	m=122;
	SA();
	return 0;
}

最小表示法

提示:本题有 \(O(n)\) 做法,后缀数组纯属乱搞。

直接把板子扔上去求出来最小后缀然后分段输出不就是了。
一交发现90分,思考将近半小时后,得到如下 hack :
5 4 0 0 0 3 2 4 1 0
可以发现,如果按照 SA 板子,会把最后一个 0 当成最小后缀,但显然有更优解 0 0 0 3 2 4 ……
怎么办,直接复制一遍数组然后再跑就是了,找最小后缀时只在前半部分找就行。

#include<iostream>

using namespace std;
const int N=6e5+10;
int n,a[N];
int sa[N],num,m;
int cnt[N],x[N],y[N];
int mid=0;
void SA(){
	for(int i=1;i<=n;i++){
		x[i]=a[i];
		cnt[x[i]]++;
	}
	for(int i=2;i<=m;i++){
		cnt[i]+=cnt[i-1];
	}
	for(int i=n;i>=1;i--){
		sa[cnt[x[i]]--]=i;
	}
	for(int k=1;k<=n;k<<=1){
		int num=0;
		for(int i=n-k+1;i<=n;i++){
			y[++num]=i;
		}
		for(int i=1;i<=n;i++)
			if(sa[i]>k)
				y[++num]=sa[i]-k;
		for(int i=1;i<=m;i++) cnt[i]=0;
		for(int i=1;i<=n;i++) cnt[x[i]]++;
		for(int i=2;i<=m;i++) cnt[i]+=cnt[i-1];
		for(int i=n;i>=1;i--){
			sa[cnt[x[y[i]]]--]=y[i];
			y[i]=0;
		}
		swap(x,y);
		x[sa[1]]=1,num=1;
		for(int i=2;i<=n;i++){
			x[sa[i]]=(y[sa[i]]==y[sa[i-1]] && y[sa[i]+k]==y[sa[i-1]+k]) ? num : ++num;
		}
		if(num==n) break;
		m=num;
	}
}
int main(){
//	freopen("5000.in","r",stdin);
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		a[i]++;
		a[i+n]=a[i];
	}
	n*=2;
	m=30;
	SA();
	n/=2;
	int mid=1;
	for(int i=1;i<=n;i++){
		if(sa[i]<=n){
			mid=sa[i];
			break;
		}
	}
	for(int i=mid;i<=mid+n-1;i++){
		cout<<a[i]-1<<' ';
	}
	return 0;
}

字符加密

如果题目要求将字符串从某处分开在首尾相连,只需要复制一遍然后在做后缀数组即可。
其他内容就是后缀数组板子了。

#include<iostream>
#include<cstring>
using namespace std;
const int N=300010;
string s,a;
int n,m;
int x[N],y[N];
int sa[N],cnt[N],num;
int len;
void SA(){
	for(int i=0;i<n;i++){
		x[i]=s[i];
		cnt[x[i]]++;
	}
	for(int i=1;i<m;i++){
		cnt[i]+=cnt[i-1];
	}
	for(int i=0;i<n;i++){
		sa[--cnt[x[i]]]=i;
	}
	for(int k=1;k<=n;k<<=1){
		int num=0;
		for(int i=n-k;i<n;i++){
			y[num++]=i;
		}
		for(int i=0;i<n;i++){
			if(sa[i]>=k) y[num++]=sa[i]-k;
		}
		for(int i=0;i<m;i++) cnt[i]=0;
		for(int i=0;i<n;i++) cnt[x[y[i]]]++;
		for(int i=1;i<m;i++) cnt[i]+=cnt[i-1];
		for(int i=n-1;i>=0;i--){
			sa[--cnt[x[y[i]]]]=y[i];
			y[i]=0;
		}
		swap(x,y);
		x[sa[0]]=0,num=1;
		for(int i=1;i<n;i++){
			x[sa[i]]=(y[sa[i]]==y[sa[i-1]] && y[sa[i]+k]==y[sa[i-1]+k]) ? num-1 : num++;
		}
		if(num==n) break;
		m=num;
	}
	return ;
}
int main(){
	cin>>s;
	len=s.length();
	n=2*len;
	m=300;
	for(int i=len;i<n;i++){
		s+=s[i-len];
	}
	SA();
	for(int i=0;i<n;i++){
		if(sa[i]<len){
			cout<<s[sa[i]+len-1];
		}
	}
	return 0;
}
posted @ 2025-04-08 16:52  Tighnari  阅读(17)  评论(2)    收藏  举报