后缀数组C++详解

发布时间 2023-08-10 19:30:45作者: 不怕困难的博客

后缀定义

“后缀i”代表以第i个字符开头的后缀,存储是用i代表字符串s的后缀s[i...n]

后缀数组是什么?

后缀数组(Suffix Array)主要关系到两个数组:sa 和 rk。

其中,sa[i] 表示将所有后缀排序后第 i 小的后缀的编号,也是所说的后缀数组,后文也称编号数组 sa;

rk[i] 表示后缀 i 的排名,是重要的辅助数组,后文也称排名数组 rk。

这两个数组满足性质:sa[rk[i]]=rk[sa[i]]=i。

解释

后缀数组示例:

image

后缀数组怎么求?

O(n^2logn) 做法
我相信这个做法大家还是能自己想到的:将盛有全部后缀字符串的数组进行 sort 排序,由于排序进行 O(n\log n) 次字符串比较,每次字符串比较要 O(n) 次字符比较,所以这个排序是 O(n^2\log n) 的时间复杂度。
O(nlog^2n) 做法
这个做法要用到倍增的思想。

首先对字符串 s 的所有长度为 1 的子串,即每个字符进行排序,得到排序后的编号数组 sa_1 和排名数组 rk_1。

倍增过程:

用两个长度为 1 的子串的排名,即 \(rk_1[i]\)\(rk_1[i+1]\),作为排序的第一第二关键字,就可以对字符串 s 的每个长度为 2 的子串:\(\{s[i\dots \min(i+1, n)]\ |\ i \in [1,\ n]\}\) 进行排序,得到 sa_2 和 rk_2;

之后用两个长度为 2 的子串的排名,即 rk_2[i] 和 rk_2[i+2],作为排序的第一第二关键字,就可以对字符串 s 的每个长度为 4 的子串:\(\{s[i\dots \min(i+3, n)]\ |\ i \in [1,\ n]\}\) 进行排序,得到 sa_4 和 rk_4;

以此倍增,用长度为 w/2 的子串的排名,即 \(rk_{w/2}[i]\)\(rk_{w/2}[i+w/2]\),作为排序的第一第二关键字,就可以对字符串 s 的每个长度为 w 的子串 \(s[i\dots \min(i+w-1,\ n)]\) 进行排序,得到 sa_w 和 rk_w。其中,类似字母序排序规则,当 i+w>n 时,\(rk_w[i+w]\) 视为无穷小;

\(rk_w[i]\) 即是子串 \(s[i\dots i + w - 1]\) 的排名,这样当 w \geqslant n 时,得到的编号数组 sa_w,也就是我们需要的后缀数组。

#include <bits/stdc++.h>
using namespace std;
const int N = 1000010;
char s[N];
int n, w, sa[N], rk[N << 1], oldrk[N << 1];
// 为了防止访问 rk[i+w] 导致数组越界,开两倍数组。
// 当然也可以在访问前判断是否越界,但直接开两倍数组方便一些。
int main() {
    int i, p;
    scanf("%s", s + 1);
    n = strlen(s + 1);
    for (i = 1; i <= n; ++i)
        sa[i] = i, rk[i] = s[i];
    for (w = 1; w < n; w <<= 1) {
        sort(sa + 1, sa + n + 1, [](int x, int y) {
            return rk[x] == rk[y] ? rk[x + w] < rk[y + w] : rk[x] < rk[y];
        }); // 这里用到了 lambda
        memcpy(oldrk, rk, sizeof(rk));
        // 由于计算 rk 的时候原来的 rk 会被覆盖,要先复制一份
        for (p = 0, i = 1; i <= n; ++i) {
            if (oldrk[sa[i]] == oldrk[sa[i - 1]] &&
                oldrk[sa[i] + w] == oldrk[sa[i - 1] + w]) {
                rk[sa[i]] = p;
            } else {
                rk[sa[i]] = ++p;
            } // 若两个子串相同,它们对应的 rk 也需要相同,所以要去重
        }
    }
    for (i = 1; i <= n; ++i)
        printf("%d ", sa[i]);
    return 0;
}