题目链接:传送门
描述
后缀数组 (SA) 是一种重要的数据结构,通常使用倍增或者DC3算法实现,这超出了我们的讨论范围。
在本题中,我们希望使用快排、Hash与二分实现一个简单的 $O(n log^2 n )$ 的后缀数组求法。
详细地说,给定一个长度为 n 的字符串S(下标 0~n-1),我们可以用整数 k(0≤k<n) 表示字符串S的后缀 S(k~n-1)。
把字符串S的所有后缀按照字典序排列,排名为 i 的后缀记为 SA[i]。额外地,我们考虑排名为 i 的后缀与排名为 i-1 的后缀,把二者的最长公共前缀的长度记为 Height[i]。
我们的任务就是求出SA与Height这两个数组。
输入格式
一个字符串,长度不超过30万。
输出格式
第一行为数组SA,相邻两个整数用1个空格隔开。
第二行为数组Height,相邻两个整数用1个空格隔开,特别地,假设Height[1]=0。
样例输入
ponoiiipoi
样例输出
9 4 5 6 2 8 3 1 7 0 0 1 2 1 0 0 2 1 0 2
样例解释
排名第一(最小)的后缀是9(S[9~9],即字符串 i),第二的是后缀4(S[4~9],即字符串iiipoi),第三的是后缀5(S[5~9],即字符串iipoi)以此类推。Height[2]表示排名第2与第1的后缀的最长公共前缀,长度为1,Height[3]表示排名第3与第2的后缀的最长公共前缀,长度为2,以此类推。
题解:
假设字符串长度为 $N$,那么如果用暴力的方式来比较两个后缀子串的字典序大小(以及求最长公共前缀的长度),显然一次比较是 $Oleft( {N} ight)$ 的时间复杂度,
如果用快排对 $N$ 个后缀子串进行排序,那么就要 $O(N^2 log N)$ 的时间复杂度,
我们尝试考虑优化的地方:比较两个字符串的时间,从 $O(N)$ 降到 $O(log N)$,
对于两个后缀子串,或者更一般的,对于两个字符串,怎么更快速的比较字典序大小,或者,怎么更快速求的最长公共前缀?
容易想到,可以二分最长公共前缀的长度,用字符串哈希 $O(1)$ 判断是否两个前缀子串是否一样,
然后判断两个字符串中哪个字典序更大就很简单了,去掉最长公共前缀,比较一下剩下的第一个字符即可。
AC代码:
#include<bits/stdc++.h> using namespace std; typedef unsigned long long ull; const int P=131; const int maxn=300000+10; char s[maxn]; int len; int sa[maxn],h[maxn]; ull pre[maxn],Ppow[maxn]; void pretreat() { pre[0]=0; Ppow[0]=1; for(int i=1;i<=len;i++) { pre[i]=pre[i-1]*P+(s[i]-'a'+1); Ppow[i]=Ppow[i-1]*P; } } inline bool isSame(int l1,int r1,int l2,int r2) { return pre[r1]-pre[l1-1]*Ppow[r1-(l1-1)] == pre[r2]-pre[l2-1]*Ppow[r2-(l2-1)]; } inline int maxpre(int a,int b) { int l=0,r=min(len-a+1,len-b+1),mid; while(l<r) { mid=(l+r)/2+1; if(isSame(a,a+mid-1,b,b+mid-1)) l=mid; else r=mid-1; } return l; } bool cmp(int a,int b) { int mp=maxpre(a,b); return s[a+mp]<=s[b+mp]; } int main() { scanf("%s",s+1); len=strlen(s+1); pretreat(); for(int i=1;i<=len;i++) sa[i]=i; sort(sa+1,sa+len+1,cmp); for(int i=1;i<=len;i++) { if(i==1) h[i]=0; else h[i]=maxpre(sa[i-1],sa[i]); printf("%d%c",sa[i]-1,i<len?' ':' '); } for(int i=1;i<=len;i++) printf("%d%c",h[i],i<len?' ':' '); }
1A开心
时间复杂度:
排序 $O(N log^2 N)$,计算Height数组 $O(N log N)$,总时间复杂度 $O(N log^2 N)$。