2715. 后缀数组
题目链接
2715. 后缀数组
给定一个长度为 \(n\) 的字符串,只包含大小写英文字母和数字。
将字符串中的 \(n\) 个字符的位置编号按顺序设为 \(1∼n\)。
并将该字符串的 \(n\) 个非空后缀用其起始字符在字符串中的位置编号表示。
现在要对这 \(n\) 个非空后缀进行字典序排序,并给定两个数组 \(SA\) 和 \(Height\)。
排序完成后,用 \(SA[i]\) 来记录排名为 \(i\) 的非空后缀的编号,用 \(Height[i]\) 来记录排名为 \(i\) 的非空后缀与排名为 \(i−1\) 的非空后缀的最长公共前缀的长度(\(1≤i≤n\))。
特别的,规定 \(Height[1]=0\)。
请你求出这两个数组。
输入格式
共一行,包含一个长度为 \(n\) 的仅包含大小写英文字母或数字的字符串。
输出格式
第一行包含 \(n\) 个整数,表示 \(SA\) 数组。
第二行包含 \(n\) 个整数,表示 \(Height\) 数组。
数据范围
\(1≤n≤10^6\)
输入样例:
abababab
输出样例:
7 5 3 1 8 6 4 2
0 2 4 6 0 1 3 5
解题思路
后缀数组,倍增
后缀数组有两个关键的数组:
在后缀形成的数组 \(x[i]\) 中,
-
\(sa[i]\) 数组表示数组 \(x\) 中 排名为 \(i\) 的数组下标
-
\(height[i]\) 表示 \(sa[i]\) 和 \(sa[i-1]\) 的最长公共前缀
\(\color{red}{如何倍增求解 sa 数组?}\)
基于基数排序的思想,设置一个桶 \(c[i]\) 表示值为 \(i\) 的数的数量,先将所有后缀子串按第一个字符排序,基数排序是稳定的,当第一个字符相等时不会改变原后缀子串的顺序,每次排序按长度排序,长度不足后面用字典序最小的字符补齐,例如前一次倍增排好了长度为 \(k\) 的子串,此时排序长度为 \(2\times k\) 的子串,由于基数排序的稳定性,整体上可以将长度为 \(2\times k\) 的子串先将后 \(k\) 个字符排序,再将前 \(k\) 个字符排序,这样可以保证对于长度为 \(2\times k\) 的子串来说,前 \(k\) 个子串相等,后 \(k\) 个子串也会是排好序的,这样长度为 \(2\times k\) 的子串就排好了序,同时可求解 \(sa\) 数组
- 时间复杂度:\(O(nlogn)\)
\(\color{red}{如何求解 height 数组?}\)
先定义函数 \(lcp(i,j)\):表示排名为 \(i\) 和 \(j\) 的最长公共前缀,则有 \(lcp(i,j)=min(lcp(i,k),lcp(k,j))\),其中 \(i<j\)
证明:先证明 \(min(lcp(i,k),lcp(k,j))\geq lcp(i,j)\),假设 \(lcp(i,j)\) 为某一段,如果 \(lcp(i,k)\) 要小于该段,显然会导致 \(k\) 不应该处在 \(i\) 和 \(j\) 之间,矛盾,故 \(lcp(i,k)\geq lcp(i,j)\),同理 \(lcp(j,k)\geq lcp(i,j)\),即 \(min(lcp(i,k),lcp(k,j))\geq lcp(i,j)\);再证明 \(min(lcp(i,k),lcp(k,j))\leq lcp(i,j)\),当 \(lcp(i,k)>lcp(i,j)\) 时,如果 \(lcp(k,j)>lcp(i,j)\),此时会导致 \(lcp(i,j)\) 不止原来这么小,所以 \(lcp(k,j)\leq lcp(i,j)\),故 \(min(lcp(i,k),lcp(k,j))\leq lcp(i,j)\),当 \(lcp(i,k)\leq lcp(i,j)\) 时,显然也有 \(min(lcp(i,k),lcp(k,j))\leq lcp(i,j)\),故 \(lcp(i,j)=min(lcp(i,k),lcp(k,j))\),很容易引申出 \(lcp(i,j)=min(lcp(i,i+1),lcp(i+1,i+2),\dots,lcp(j-1,j))\)
再定义数组 \(h[i]\) 表示下标为 \(i\) 的后缀和排名为 \(sa[i]-1\) 的后缀的最长公共前缀,则有 \(h[i]\geq h[i-1]-1\)
证明:假设第 \(i-1\) 个后缀的排名比第 \(i\) 个后缀靠前,靠后的话也是同样考虑,如果 \(h[i-1]=0\) 即第 \(i-1\) 个后缀与前一个排名的后缀的第一个字符不相等,显然有 \(h[i]\geq 0-1\),否则第 \(i-1\) 个后缀除去第一个字符正好对应第 \(i\) 个后缀,考虑第 \(i-1\) 个后缀的前一个排名的后缀除去第一个字符后的后缀 \(k\),除去第一个后缀后,两者顺序不变,第 \(i-1\) 个后缀变为第 \(i\) 个后缀,所以此时 \(k\) 在 \(i\) 的前面,由上面的定理,\(lcp(k,i)=min(lcp(k,k+1),lcp(k+1,k+2),\dots,lcp(i-1,i))\),即有 \(lcp(k,i)\leq lcp(i-1,i)\),即 \(h[i-1]-1\leq h[i]\)
定义数组 \(rk[i]\) 表示下标为 \(i\) 的排名,则 \(rk[sa[i]]=i\)
而又有 \(height[rk[i]]=h[i]\),求出 \(h\) 等价于求出 \(height\) 数组
- 时间复杂度:\(O(n)\)
代码
// Problem: 后缀数组
// Contest: AcWing
// URL: https://www.acwing.com/problem/content/2717/
// Memory Limit: 64 MB
// Time Limit: 1000 ms
//
// Powered by CP Editor (https://cpeditor.org)
// %%%Skyqwq
#include <bits/stdc++.h>
//#define int long long
#define help {cin.tie(NULL); cout.tie(NULL);}
#define pb push_back
#define fi first
#define se second
#define mkp make_pair
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
typedef pair<LL, LL> PLL;
template <typename T> bool chkMax(T &x, T y) { return (y > x) ? x = y, 1 : 0; }
template <typename T> bool chkMin(T &x, T y) { return (y < x) ? x = y, 1 : 0; }
template <typename T> void inline read(T &x) {
int f = 1; x = 0; char s = getchar();
while (s < '0' || s > '9') { if (s == '-') f = -1; s = getchar(); }
while (s <= '9' && s >= '0') x = x * 10 + (s ^ 48), s = getchar();
x *= f;
}
const int N=1e6+5;
int n,m,cnt,c[N],x[N],y[N],sa[N],rk[N],height[N];
char s[N];
void get_sa()
{
for(int i=1;i<=n;i++)c[x[i]=s[i]]++;
for(int i=2;i<=m;i++)c[i]+=c[i-1];
for(int i=n;i>=1;i--)sa[c[x[i]]--]=i;
for(int k=1;k<=n;k<<=1)
{
cnt=0;
for(int i=n-k+1;i<=n;i++)y[++cnt]=i;
for(int i=1;i<=n;i++)
if(sa[i]>k)y[++cnt]=sa[i]-k;
for(int i=1;i<=m;i++)c[i]=0;
for(int i=1;i<=n;i++)c[x[i]]++;
for(int i=2;i<=m;i++)c[i]+=c[i-1];
for(int i=n;i>=1;i--)sa[c[x[y[i]]]--]=y[i],y[i]=0;
//y[i]表示排名为i的下标,x[y[i]]表示排名为i的权值
swap(x,y);
x[sa[1]]=1,cnt=1;
for(int i=2;i<=n;i++)
x[sa[i]]=(y[sa[i-1]]==y[sa[i]]&&y[sa[i-1]+k]==y[sa[i]+k])?cnt:++cnt;
if(cnt==n)break;
m=cnt;
}
}
void get_height()
{
for(int i=1;i<=n;i++)rk[sa[i]]=i;
for(int i=1,k=0;i<=n;i++)
{
if(rk[i]==1)continue;
if(k)k--;
int j=sa[rk[i]-1];
while(i+k<=n&&s[i+k]==s[j+k])k++;
height[rk[i]]=k;
}
}
int main()
{
scanf("%s",s+1);
n=strlen(s+1);
m='z';
get_sa();
get_height();
for(int i=1;i<=n;i++)printf("%d ",sa[i]);
puts("");
for(int i=1;i<=n;i++)printf("%d ",height[i]);
return 0;
}