后缀数组(suffix array)是解决字符串问题非常有力的工具,主要是为了得到三个数组,分别是sa[i],rank[i],height[i]。先对字符串的各后缀进行排序,sa[i]表示排序后第i名的后缀它开始的位置,rank[i]表示从i这个位置往后的后缀它的排名是多少,显然sa和rank数组互逆,如果已知sa[i] = t,则rank[t] = i,height[i]表示排完序后第i个后缀和第i-1个后缀的lcp(最长公共前缀)长度。
具体算法实现大致可分为两类,一种是基于倍增+基数排序的思想,复杂度为O(nlogn),另一种是用DC3算法,代码比较麻烦,复杂度为O(n)。一般情况下用nlogn的方法就可以,比较好写而且非极端情况下不会被卡掉(其实还有更好写的二分+hash,nlogn*logn的复杂度,cmp里面二分判断字典序大小)。
放一个倍增的模板:
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e6+10;
int n, m;
char s[maxn];
int sa[maxn], height[maxn], x[maxn], y[maxn], rk[maxn], tong[maxn];
void get_sa()
{
for(int i = 1; i <= m; i++) tong[i] = 0;
for(int i = 1; i <= 2*n; i++) x[i] = y[i] = 0;
for(int i = 1; i <= n; i++) tong[x[i] = s[i]] ++;//初始第一关键字的排名就设置为其ASCII码即可
for(int i = 2; i <= m; i++) tong[i] += tong[i-1];//计算前缀和,注意只算到m即可
for(int i = n; i; i--) sa[tong[x[i]]--] = i;//以第一关键字计算排名
for(int k = 1; k <= n; k <<= 1) {//倍增计算sa
int num = 0;//计算Y,排名从1开始,指针指向0
//Y[i] = j -> 以第二关键字排名,排名为i的首字母下标为j(即第j个后缀)
for(int i = n-k+1; i <= n; i++) y[++num] = i;//后k个没有第二关键字,所以排名最小先分配排名
for(int i = 1; i <= n; i++) {
if(sa[i] <= k) continue;//前k个第一关键字不能作为某个后缀的第二关键字
y[++num] = sa[i] - k;//sa为以第一关键字计算的排名,从小到大枚举排名,对应的下标其实是第 sa[i] - k 个后缀的第二关键字
}
for(int i = 0; i <= m; i++) tong[i] = 0;//初始化桶,范围为1-m即可
for(int i = 1; i <= n; i++) tong[x[i]]++;//计算新的第一关键字每个排名有几个
for(int i = 2; i <= m; i++) tong[i] += tong[i-1];//计算前缀和,1-m即可
for(int i = n; i; i--) sa[tong[x[y[i]]]--] = y[i], y[i] = 0;
//y[i] 表示以第二关键字排序,排名为i的后缀的下标
//x[y[i]] 表示上述后缀按照第一关键字的排名
//tong[x[y[i]]] 表示小于等于上述排名的数量,也就是该后缀的排名
//sa记录上述排名对应的下标为 y[i],从后往前枚举第二关键字的排名,使得第一关键字相同的后缀也可以依靠第二关键字区分
for(int i = 0; i <= 2*num; i++)
{
int temp = x[i];
x[i] = y[i];
y[i] = temp;
}//接下来要更新X数组,且要用到旧的X数组,Y数组接下里用不到
//则将两者交换,目的是将旧的X存到Y中,后面所有的Y实际就是旧的X
// 将当前的第一关键字和第二关键字当做下一轮的第一关键字,sa中存的就是按照双关键字排序的结果。
//swap比较慢,换成1~2*num的循环会快很多!
x[sa[1]] = 1, num = 1; //sa[1]对于的后缀新X的排名也为1
for(int i = 2; i <= n; i++)
//如果新排名为i的后缀和新排名为i-1的后缀的第一关键字排名相同(前一个 == )
//并且它们的第二关键字排名也相同(后一个 == ),那么两个后缀的新X排名相同,否则不同
x[sa[i]] = (y[sa[i]] == y[sa[i-1]] && y[sa[i] + k] == y[sa[i-1] + k]) == 1 ? num : ++ num;
if(n == num) return;//如果已经完全区分出n个后缀了,则可以结束循环
m = num;//更新离散化后的rank范围
}
}
//height[i] 表示排名为i和排名为i-1的后缀的最长公共前缀
void get_height()
{
for(int i = 1; i <= n; i++) rk[sa[i]] = i;//rk[i] = j - > 第i个后缀的排名为j
for(int i = 1, k = 0; i <= n; i++) {
if(rk[i] == 1) continue;//排名为1的height不用计算
//设h[i]表示height[rk[i]],即位于第i个的后缀与排名在它前一个的后缀的最长公共前缀
if(k) k--;//由于h[i]>=h[i-1]-1,所以从k-1开始枚举
int j = sa[rk[i]-1];//排名在i前一个的后缀的下标
while(i + k <= n && j + k <= n && s[i+k] == s[j+k]) k++;//如果相等,则最长公共前缀+1
height[rk[i]] = k;//更新height
}
}
void solve()
{
scanf("%s", s+1); //下标从1开始
n = strlen(s+1);
//m每次都需要初始化,因为过程中会被更改
m = 'z';//字符集的最大值,字符集最小值为1!
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 ", rk[i]);
puts("");
for(int i = 1; i <= n; i++) printf("%d ", height[i]);
}
signed main()
{
int t = 5;
while(t--)
solve();
return 0;
}
/*
Input:
abab
排序后:
ab id:3 rk:1
abab id:1 rk:2
b id:4 rk:3
bab id:2 rk:4
*/