后缀数组 (
首先来了解一些基本概念:
子串
在字符串
后缀
后缀即字符串中某个位置
后缀数组
把 sa[i] 就表示排名为
另外一个辅助数组 rank[i] 代表起始位置下标为
两者可以互相映射。
基数排序
一个时间复杂度接近
基数排序思路很简单,就是把数的每一位进行一遍桶排,最后即可得到答案。
例如如下数据:
| 数据 | 12 | 34 | 56 | 15 | 31 | 52 |
|---|---|---|---|---|---|---|
| 排名 | 1 | 4 | 6 | 2 | 3 | 5 |
具体是这样排的:
先按个位进行桶排序:
| 数据 | 31 | 12 | 52 | 34 | 15 | 56 |
|---|---|---|---|---|---|---|
| 个位数 | 1 | 2 | 2 | 4 | 5 | 6 |
再按十位对新得到的序列进行桶排序:
| 数据 | 12 | 15 | 31 | 34 | 52 | 56 |
|---|---|---|---|---|---|---|
| 十位数 | 1 | 1 | 3 | 3 | 5 | 5 |
代码:
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<cmath>
#include<algorithm>
#include<ctime>
#define INF 1e9
using namespace std;
const int maxn=2e5;
int n,a[maxn],b[maxn],t[11],maxx,p;
int getbit()//得到最大数的位数
{
for(int i=1;i<=n;i++)
maxx=max(maxx,a[i]);
while(maxx)
{
maxx/=10;
p++;
}
return p;
}
void radixsort()
{
p=getbit();
int radix=1;
for(int i=1;i<=p;i++)
{
for(int j=0;j<=9;j++)
t[j]=0;//初始化桶
for(int j=1;j<=n;j++)
{
int k=(a[j]/radix)%10;
t[k]++;//往桶里添加数
}
for(int j=1;j<=9;j++)
t[j]=t[j-1]+t[j];//更新排名,计算比j小的数有多少个
for(int j=n;j>=1;j--)
{
int k=(a[j]/radix)%10;
b[t[k]]=a[j];//排答案
t[k]--;
}
for(int j=1;j<=n;j++)
a[j]=b[j];//复制
radix*=10;//扩大10倍继续排下一位
}
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
radixsort();
for(int i=1;i<=n;i++)
printf("%d ",a[i]);
return 0;
}
接下来就是重点内容了。
后缀数组主要有两种求法:倍增和
倍增 算法
此处引图和思路来源一个大佬的博客。
流程图如下所示:
例如我们有一个字符串为 banana。
我们首先对单个字符排序(可理解为对每一个后缀的第一个字符排序,与后面进行衔接),如图:

对于每个字母我们用字典序给了一个名次,即:
| 字母 | |||
|---|---|---|---|
| 名次 | 1 | 2 | 3 |
接下来运用倍增思想,对每个后缀的前两个字符排序,以第一个字符为第一关键字,第二个字符为第二关键字即可,再重新基数排序排名。

然后是

由于字符串长度小于 rank 数组,如下:
| 1 | 2 | 3 | 4 | 5 | 6 | |
|---|---|---|---|---|---|---|
| 4 | 3 | 6 | 2 | 5 | 1 |
同时在排序时可以顺便求出
| 1 | 2 | 3 | 4 | 5 | 6 | |
|---|---|---|---|---|---|---|
| 6 | 4 | 2 | 1 | 5 | 3 |
知道了思路,怎么代码实现?
像放出全部代码:
void radixsort(int a[],int b[])//基数排序
{
for(int i=1;i<=m+1;i++)
t[i]=0;//初始化
for(int i=1;i<=n;i++)
t[a[i]]++;//统计
for(int i=1;i<=m;i++)
t[i]+=t[i-1];//更新
for(int i=n;i>=1;i--)
sa[t[a[b[i]]]--]=b[i];
}
bool check(int r[],int a,int b,int j)
{
return r[a]==r[b]&&r[a+j]==r[b+j];
}
void get_SA(int a[],int b[])
{
for(int i=1;i<=n;i++)
{
m=max(m,a[i]=s[i]);//赋值,求出最大数
b[i]=i;//初始顺序
}
radixsort(a,b);
for(int j=1;j<=n;j<<=1)//倍增长度
{
int p=0;
for(int i=1;i<=j;i++)
b[++p]=n-j+i;
for(int i=1;i<=n;i++)
if(sa[i]>j)
b[++p]=sa[i]-j;
radixsort(a,b);
int *t=a;
a=b;
b=t;
a[sa[1]]=p=1;
for(int i=2;i<=n;i++)
a[sa[i]]=check(b,sa[i],sa[i-1],j)?p:++p;
if(p>=n)
break;
m=p;
}
}
我们依次解释:
void radixsort(int a[],int b[])//基数排序
{
for(int i=1;i<=m+1;i++)
t[i]=0;//初始化
for(int i=1;i<=n;i++)
t[a[i]]++;//统计
for(int i=1;i<=m;i++)
t[i]+=t[i-1];//更新
for(int i=n;i>=1;i--)
sa[t[a[b[i]]]--]=b[i];
}
这是基数排序的代码。
首先先将桶清空,然后将
最后倒序,将
for(int i=1;i<=n;i++)
{
m=max(m,a[i]=s[i]);//赋值,求出最大数
b[i]=i;//初始顺序
}
radixsort(a,b);
for(int j=1;j<=n;j<<=1)//倍增长度
{
int p=0;
for(int i=1;i<=j;i++)
b[++p]=n-j+i;
for(int i=1;i<=n;i++)
if(sa[i]>j)
b[++p]=sa[i]-j;
radixsort(a,b);
···
}
接着枚举
由于从
接着遍历一遍,如果
···
radixsort(a,b);
int *t=a;
a=b;
b=t;
a[sa[1]]=p=1;
for(int i=2;i<=n;i++)
a[sa[i]]=check(b,sa[i],sa[i-1],j)?p:++p;
if(p>=n)
break;
m=p;
}
然后进行第二次排序,得到新的
接着交换
很明显
从 check 函数,如下所示:
bool check(int r[],int a,int b,int j)
{
return r[a]==r[b]&&r[a+j]==r[b+j];
}
结合 check 函数,因为
此时得到的 rank 值,直接结束即可,否则令
整个算法时间复杂度为
最长公共前缀
定义一个新的数组
定义
对于任意
那么如何快速求出
令
证明略过,自己上网查。
具体实现:
void get_high(int sa[])
{
for(int i=1;i<=n;i++)
rank[sa[i]]=i;//反推rank
int j=0;
for(int i=1;i<=n;i++)
{
if(j)
j--;
while(i+j<=n&&sa[rank[i]-1]+j<=n&&s[i+j]==s[sa[rank[i]-1]+j])
j++;
height[rank[i]]=j;
}
}
首先通过得到的
if(j)
j--;
这个其实是上面性质
接着就是粗暴的按照每位依次对比,如果相同长度加
综上,基本的后缀数组就讲完了,后缀数组可以完成很多有用的操作,是一个很厉害的数据结构。
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<cmath>
#include<algorithm>
#include<ctime>
#define INF 1e9
using namespace std;
const int maxn=2e6;
int n,m,k;
char s[maxn];
int sa[maxn],t[maxn];
int a[maxn],b[maxn];
void radixsort(int a[],int b[])//基数排序
{
for(int i=1;i<=m+1;i++)
t[i]=0;//初始化
for(int i=1;i<=n;i++)
t[a[i]]++;//统计
for(int i=1;i<=m;i++)
t[i]+=t[i-1];//更新
for(int i=n;i>=1;i--)
sa[t[a[b[i]]]--]=b[i];
}
bool check(int r[],int a,int b,int j)
{
return r[a]==r[b]&&r[a+j]==r[b+j];
}
void get_SA(int a[],int b[])
{
for(int i=1;i<=n;i++)
{
m=max(m,a[i]=s[i]);//赋值,求出最大数
b[i]=i;//初始顺序
}
radixsort(a,b);
for(int j=1;j<=n;j<<=1)//倍增长度
{
int p=0;
for(int i=1;i<=j;i++)
b[++p]=n-j+i;
for(int i=1;i<=n;i++)
if(sa[i]>j)
b[++p]=sa[i]-j;
radixsort(a,b);
int *t=a;
a=b;
b=t;
a[sa[1]]=p=1;
for(int i=2;i<=n;i++)
a[sa[i]]=check(b,sa[i],sa[i-1],j)?p:++p;
if(p>=n)
break;
m=p;
}
}
int main()
{
scanf("%s",s+1);
n=strlen(s+1);
get_SA(a,b);
for(int i=1;i<=n;i++)
printf("%d ",sa[i]);
return 0;
}
接着放一些有用的资料和博客,引用了不少:
感谢以上大佬。
st=>start: 开始
p1=>inputoutput: 把每个字母排序得到第一次的 sa 值
op=>operation: 根据上一次的 sa 值得到第二关键字
p2=>inputoutput: 按现在关键字排序得到新的 sa 值
p3=>inputoutput: 根据现在的 sa 值得到新的第一关键字
p4=>inputoutput: 重新排序
cond=>condition: 排名各不相同?
e=>end: 结束
st->p1->op->p2->p3->cond
cond(yes)->e
cond(no)->p4->op