倍增求后缀数组 (Suffix Array)
倍增求后缀数组 (Suffix Array)
定义一个字符串第
朴素
将所有后缀枚举出来, 字符不足的用空字符补全, 重载字符串比较运算符, 快速排序. 一次比较时间复杂度是字符串长度, 也就是
桶排序 (Bucket Sort)
假设
这时
建一个临时数组
相当于在
最后将排好序的数组从
时空复杂度
基数排序 (Radix Sort)
在
它的做法是将先将整数表示成
按最低位为关键字进行桶排序后, 按第二位为关键字桶排序时, 这时待排序数组已经按第一位排好序了, 所以在往
假设现在排序的是第
因为对
上面说的是 LSD (Least significant digital) 基数排序的原理, 即从低位到高位的排序. 还有 MSD (Most significant digital) 的版本, 即从高位到低位, 二者有常数差距, 但原理相同.
倍增
对于字符串的排序, 相当于一些大整数排序, 这时如果直接用基数排序, 取构成字符串的字符集为底数
我们在以第一个字符为关键字桶排序后, 每个后缀的首字符的大小关系就已经确定了, 而第
接下来考虑倍增, 首先考虑在已经对所有后缀以它们的前
接下来, 要想将得到按前
要排序的次数是
实现
模板: luogu3809
首先是字符串处理, 因为字符集是大小写字母和数字, 所以一共是
输入
cin.getline(Inch, 1000001);
n = strlen(Inch);
for (register unsigned i(0); i < n; ++i) {
if(Inch[i] <= '9' && Inch[i] >= '0') {
Inch[i] -= 47;
continue;
}
if(Inch[i] <= 'Z' && Inch[i] >= 'A') {
Inch[i] -= 53;
continue;
}
if(Inch[i] <= 'z' && Inch[i] >= 'a') {
Inch[i] -= 59;
continue;
}
}
for (register unsigned i(0); i < n; ++i) {
Bucket[Inch[i]] = 1;
}
for (register unsigned i(0); i < 64; ++i) {
if(Bucket[i]) {
Tmpch[i] = ++Cnt; // 让桶从 1 开始, 空出 0 的位置
Bucket[i] = 0;
}
}
for (register unsigned i(0); i < n; ++i) { // 将字符串离散化成整数序列
S[i + 1].RK = Tmpch[Inch[i]]; // 字符串读入是 [0, n) 的, 题意中字符串是 (0, n] 的
}
接下来就是倍增了, 外层循环枚举当前倍增的阶段, 即最后一次排序的关键字长度. 每次预处理好本次基数排序的第二关键字, 然后基数排序.
for (register unsigned i(1); i <= n; i <<= 1) { // 当前按前 i 个字符排完了, 每次 i 倍增
for (register unsigned j(1); j + i <= n; ++j) { // 针对第二关键字不为 0 的
S[j].SubRK = S[j + i].RK;
}
for (register unsigned j(n - i + 1); j <= n; ++j) {
S[j].SubRK = 0; // 第二关键字为 0
}
RadixSort();
}
最后统计答案, 利用
for (register unsigned i(1); i <= n; ++i) {
b[S[i].RK] = i;
}
for (register unsigned i(1); i <= n; ++i) {
printf("%u ", b[i]);
}
下面来看看 RadixSort 的实现
这里用的是 LSD 基数排序, 即先按第二关键字
更多细节参见代码注释.
void RadixSort () {
unsigned MX(0); // 记录最大键值
for (register unsigned i(1); i <= n; ++i) {
++Bucket[S[i].SubRK]; // 第二关键字入桶
MX = max(S[i].SubRK, MX);
}
sumBucket[0] = 0;
for (register unsigned i(1); i <= MX; ++i) { // 求前缀和以确定在排序后的序列中的位置
sumBucket[i] = sumBucket[i - 1] + Bucket[i - 1]; // 求桶前缀和, 前缀和右边界是开区间, 所以计算的是比这个键值小的所有元素个数
Bucket[i - 1] = 0; // 清空桶
}
Bucket[MX] = 0;
for (register unsigned i(1); i <= n; ++i) { // 排好的下标存到 b 中, 即 b[i] 为第 i 小的后缀编号
b[++sumBucket[S[i].SubRK]] = i; // 前缀和自增是因为
}
b[0] = 0; // 边界 (第 0 小的不存在)
for (register unsigned i(1); i <= n; ++i) {
a[i] = b[i];
}
MX = 0;
for (register unsigned i(1); i <= n; ++i) {
++Bucket[S[i].RK]; // 第一关键字入桶
MX = max(S[i].RK, MX);
}
sumBucket[0] = 0;
for (register unsigned i(1); i <= MX; ++i) {
sumBucket[i] = sumBucket[i - 1] + Bucket[i - 1];
Bucket[i - 1] = 0;
}
Bucket[MX] = 0;
for (register unsigned i(1); i <= n; ++i) {
b[++sumBucket[S[a[i]].RK]] = a[i]; // 由于 a[i] 是 b[i] 的拷贝, 表示第 i 小的后缀编号, 所以枚举 i 一定是从最小的后缀开始填入新意义下的 b
}
b[0] = 0;
Cnt = 0; // 使 RK 不那么分散
for (register unsigned i(1); i <= n; ++i) {
if(S[b[i]].SubRK != S[b[i - 1]].SubRK || S[b[i]].RK != S[b[i - 1]].RK) {
a[b[i]] = ++Cnt; // 第 i 小的后缀和第 i - 1 小的后缀不等排名不等
}
else {
a[b[i]] = Cnt; // 第 i 小的后缀和第 i - 1 小的后缀相等排名也相等
}
}
for (register unsigned i(1); i <= n; ++i) {
S[i].RK = a[i]; // 将 a 中暂存的新次序拷贝回来
}
return;
}
反向优化
一开始没考虑下标从
由于看这个细节不爽, 所以就将原字符串预处理成
思考跑得慢的原因, 发现当排序后的
然后将程序从
所以以后只要 AC 了就别瞎优化, 否则你也能创造奇迹......