后缀数组
关键是如何得到sa数组。
采用倍增和基数排序的方法。
因为字符集一般较小, 所以基数排序是首选。
关键是如何倍增处理。 其中的关键思路是二元组递推排序。
二元组排序
考虑多个二元组(x, y), 按照以x为第一关键字, 以y为第二关键字排序。 可以先按y排序, 后按x排序。
倍增过程
字典序的比较是从前向后不断进行的, 因此不断倍增所有后缀前缀的长度。 然后进行排序。
考虑当前要求后缀的前缀长度为k时后缀的排名。 将前缀分为两部分, 构成一个二元组。
先给后部分排序。 如果后部分不存在, 则放在前面。 然后由sa得到剩下的后缀的排序。 同时将编号向前调整。
之后给前缀排序, 排完序后得到sa。
然后将二元组一一映射, 得到新的排序依据。
具体实现关键是x数组和y数组。 x数组存的是上一轮前缀排名的映射值, y为按第二关键字,即当前处理字符串的后缀排序后的后缀编号, 可以直接由sa得到。
具体流程如下:
- 将一个字符排序, x的映射值之间设为字符值即可, 然后用基数排序得到前缀为1的sa排名。
- 将要排序的前缀长度调整扩大二倍, 首先排前缀的后一半。 先把后一半长度不够的放在前面, 前一半的根据sa数组直接排, 将编号大于k的编号减少k, 调整到前缀的起点。
- 之后给前缀的前一半基数排序。 由于后一半已经有序, 只关心前一半即可。
- 根据sa更新x数组, 因为sa内部是有序的, 如果sa[i]和sa[i-1]的前半部分后缀和后半部分后缀相同, 排名设置也相同, 否则排名加一。
- 判断排名是否到了n, 如果没有说明有前缀相同的, 便继续第2步。
具体流程便是倍增长度, 排后半部分, 排前半部分, 更新排序依据, 判断是否结束。
更多细节见代码注释。
代码来自《算法竞赛入门经典——训练指南》, 建议看着书上的图阅读代码, 代码注释中会以该图为例详细解释。
void build_sa(int m) {
//PS:
//过程是不断对所有后缀的前缀排序, 且将后缀分为长度相等的前缀和后缀, 所以下面统一简称这两部分为前缀与后缀。
//x数组为已处理部分的排序映射。即书中最顶上那一组数。不过是从0开始。
//y数组为按照第二关键字,即按前缀的后半部分排序后的后缀编号。 初始可视为 y[i] = i;
int i, *x = t, *y = t2;
//首先处理前缀为1, c数组用于为基数排序。
for(i = 0; i < m; i++) c[i] = 0;
for(i = 0; i < n; i++) c[x[i] = s[i]]++;//这里无论字符集是否确定,都这么写就可以,不过m要足够大。 后面会统一映射到0-n。
for(i = 1; i < m; i++) c[i] += c[i-1];
for(i = n-1; i >= 0; i--) sa[--c[x[i]]] = i;
//可以将注释的代码取消注释,对照书中的图详细观察。 不过x书中不会和书中一样,因为从0开始编号。 不过对结果和过程理解不会影响。
// puts("0
sa: ");
// for(i = 0; i < n; i++) printf("%d ", sa[i]);
// puts("");
for(int k = 1; k <= n; k <<= 1) {
int p = 0;
// printf("
%d
", k);
// printf("x: ");
// for(i = 0; i < n; i++) printf("%d ", x[i]);
//按照第二关键字,即后缀排序。
//后面长度不够的放在前面,也就是所谓的“补0”。
for(i = n-k; i < n; i++) y[p++] = i;
//在上一轮已经处理了排名, 直接拿来用,不过当前排序的是后缀, 因此编号要向前调整k。
for(i = 0; i < n; i++) if(sa[i] >= k) y[p++] = sa[i]-k;
//这里对照书的图例,y[i]已经按照后半部分排好序。
// printf("
y %d:", p);
// for(i = 0; i < p; i++) printf("%d ", y[i]);
// printf("
x[y[i]]:");
// for(i = 0; i < n; i++) printf("%d ", x[y[i]]);
//给前缀排序,得到最终排名
for(i = 0; i < m; i++) c[i] = 0;
for(i = 0; i < n; i++) c[x[y[i]]]++;
for(i = 0; i < m; i++) c[i] += c[i-1];
for(i = n-1; i >= 0; i--) sa[--c[x[y[i]]]] = y[i];
// printf("
sa: ");
// for(i = 0; i < n; i++) printf("%d ", sa[i]);
//需要用x数组和sa数组更新x数组, y数组已经无用, 用来备份x数组。
//排名从0开始,二元组相同排名相同,否则排名增加。 因为sa中已经有序。
swap(x, y);
p = 1; x[sa[0]] = 0;
for(i = 1; i < n; i++) {
x[sa[i]] = y[sa[i-1]] == y[sa[i]] && y[sa[i-1]+k] == y[sa[i]+k] ? p-1: p++;
}
if(p >= n) break;
m = p;
}
}