后缀数组学习笔记

  • 基数排序

利用桶的单调性,从低位到高位依次将整数放到对应数位的桶中。

  • 后缀数组

定义:对于字符串 ss,定义 sa[i]sa[i] 表示 ssnn 个后缀按字典序排序后的第 ii 个后缀在 ss 中的下标rk[i]rk[i] 表示从 sis_i 开始的后缀在后缀数组中的下标。

  • 倍增求 sasa

不妨设 saw,isa_{w,i} 表示只取每个后缀的前 ww 个字符排序后第 ii 个后缀在 ss 中的下标。

考虑通过 sawsa_w 求出 sa2wsa_{2w}

容易发现如果 rkw,i<rkw,jrk_{w,i}<rk_{w,j} 那么 s[i,,i+w1]s[i,\dots,i+w-1] 一定小于 s[j,,j+w1]s[j,\dots,j+w-1]

所以可以把每个长度为 2w2w 的子串拆成两个长度为 ww 的子串。

那么判断两个长度为 2w2w 的子串 s[i,,i+2w1]s[i,\dots,i+2w-1]s[j,,j+2w1]s[j,\dots,j+2w-1] 的字典序就只要判断 rkw,i,rkw,i+w,rkw,j,rkw,j+wrk_{w,i},rk_{w,i+w},rk_{w,j},rk_{w,j+w} 之间的关系。

如果 iijj 小,那么 rkw,i<rkw,jrk_{w,i}<rk_{w,j} 或者 rkw,i=rkw,jrk_{w,i}=rk_{w,j} 并且 rkw,i+w<rkw,j+wrk_{w,i+w}<rk_{w,j+w}

所以可以直接双关键字排序。

时间复杂度:O(nlog2n)O(n\log^2 n)

代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <bits/stdc++.h>

// #define int int64_t

const int kMaxN = 1e6 + 5;

int n;
int sa[kMaxN << 2], rk[kMaxN << 2], nrk[kMaxN << 2];
std::string s;

void dickdreamer() {
std::cin >> s;
n = s.size();
s = " " + s;
for (int i = 1; i <= n; ++i) {
sa[i] = i;
rk[i] = s[i];
}
for (int w = 1; w <= n; w <<= 1) {
auto cmp = [&] (const int x, const int y) {
return rk[x] == rk[y] ? rk[x + w] < rk[y + w] : rk[x] < rk[y];
};
std::sort(sa + 1, sa + 1 + n, cmp);
int c = 0;
for (int i = 1; i <= n; ++i)
nrk[sa[i]] = (rk[sa[i]] == rk[sa[i - 1]] && rk[sa[i] + w] == rk[sa[i - 1] + w] ? c : ++c);
for (int i = 1; i <= n; ++i)
rk[i] = nrk[i];
}
for (int i = 1; i <= n; ++i)
std::cout << sa[i] << ' ';
}

int32_t main() {
#ifdef ORZXKR
freopen("in.txt", "r", stdin);
freopen("out.txt", "w", stdout);
#endif
std::ios::sync_with_stdio(0), std::cin.tie(0), std::cout.tie(0);
int T = 1;
// std::cin >> T;
while (T--) dickdreamer();
// std::cerr << 1.0 * clock() / CLOCKS_PER_SEC << "s\n";
return 0;
}
  • 优化

容易发现像上面那样求 rkrk,每个 rkrk 的值域是 [1,n][1,n],所以直接先对第二关键字排序,再对第一关键字排序即可。

时间复杂度:O(nlogn)O(n\log n)

代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
void suffix_sort(std::string s, int *sa, int *rk) {
static int cnt[kMaxN], ork[kMaxN << 1], id[kMaxN];
memset(cnt, 0, sizeof(cnt));
int n = static_cast<int>(s.size()) - 1;
for (int i = 1; i <= n; ++i) {
rk[i] = s[i];
++cnt[rk[i]];
}
for (int i = 1; i <= 128; ++i)
cnt[i] += cnt[i - 1];
for (int i = n; i; --i)
sa[cnt[rk[i]]--] = i;
for (int i = 1; i <= n; ++i)
ork[i] = rk[i];
int m = 0;
for (int i = 1; i <= n; ++i) {
if (ork[sa[i]] == ork[sa[i - 1]]) {
rk[sa[i]] = m;
} else {
rk[sa[i]] = ++m;
}
}
for (int w = 1; w < n; w <<= 1) {
memset(cnt, 0, sizeof(cnt));
for (int i = 1; i <= n; ++i)
id[i] = sa[i];
for (int i = 1; i <= n; ++i)
++cnt[rk[id[i] + w]];
for (int i = 1; i <= m; ++i)
cnt[i] += cnt[i - 1];
for (int i = n; i; --i)
sa[cnt[rk[id[i] + w]]--] = id[i];

memset(cnt, 0, sizeof(cnt));
for (int i = 1; i <= n; ++i)
id[i] = sa[i];
for (int i = 1; i <= n; ++i)
++cnt[rk[id[i]]];
for (int i = 1; i <= m; ++i)
cnt[i] += cnt[i - 1];
for (int i = n; i; --i)
sa[cnt[rk[id[i]]]--] = id[i];

for (int i = 1; i <= n; ++i)
ork[i] = rk[i];
m = 0;
for (int i = 1; i <= n; ++i) {
if (ork[sa[i]] == ork[sa[i - 1]] && ork[sa[i] + w] == ork[sa[i - 1] + w]) {
rk[sa[i]] = m;
} else {
rk[sa[i]] = ++m;
}
}
}
}

但是这么做常数很大,并且实测还没直接 sort 快,因为做两次基数排序是很慢的。

注意到第一次排序相当于把 i+w>ni+w>nii 放前面,并且把其他的 ii 按照 rk[i+w]rk[i+w] 的顺序排序。

又因为原来的 sasa 数组就已经按照 rkrk 排好序了,所以直接从前往后扫 sasa 数组,如果 sa[i]>wsa[i]>w 就把 sa[i]wsa[i]-w 放到新数组中即可。

还有个小优化是如果当前的 rkrk 总共出现 nn 次就说明已经排序完成,那么 break 就可以了。

这样做就比直接 sort 要快很多了。

代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
void getsa(std::string s, int *sa, int *rk) {
static int cnt[kMaxN], ork[kMaxN << 1], id[kMaxN];
memset(cnt, 0, sizeof(cnt));
int n = static_cast<int>(s.size()) - 1, m = 0;
for (int i = 1; i <= n; ++i) {
rk[i] = s[i];
++cnt[rk[i]];
}
for (int i = 1; i <= 128; ++i)
cnt[i] += cnt[i - 1];
for (int i = n; i; --i)
sa[cnt[rk[i]]--] = i;
std::copy_n(rk + 1, n, ork + 1);
for (int i = 1; i <= n; ++i) {
if (ork[sa[i]] == ork[sa[i - 1]]) {
rk[sa[i]] = m;
} else {
rk[sa[i]] = ++m;
}
}
for (int w = 1; m < n; w <<= 1) {
int p = 0;
for (int i = n - w + 1; i <= n; ++i)
id[++p] = i;
for (int i = 1; i <= n; ++i)
if (sa[i] > w)
id[++p] = sa[i] - w;
std::fill_n(cnt + 1, n, 0);
for (int i = 1; i <= n; ++i)
++cnt[rk[id[i]]];
for (int i = 1; i <= m; ++i)
cnt[i] += cnt[i - 1];
for (int i = n; i; --i)
sa[cnt[rk[id[i]]]--] = id[i];

m = 0;
std::copy_n(rk + 1, n, ork + 1);
for (int i = 1; i <= n; ++i) {
if (ork[sa[i]] == ork[sa[i - 1]] && ork[sa[i] + w] == ork[sa[i - 1] + w]) {
rk[sa[i]] = m;
} else {
rk[sa[i]] = ++m;
}
}
}
}
  • heightheight 数组

定义:sa[i1]sa[i-1]sa[i]sa[i] 的最长公共前缀长度。

  • heightheight

引理:height[rk[i]]height[rk[i1]]1height[rk[i]]\geq height[rk[i-1]]-1

证明

height[rk[i]]height[rk[i]] 就是 sis_isasaii 前面的后缀的 LCP,height[rk[i1]]height[rk[i-1]]si1s_{i-1}sasai1i-1 前面的后缀的 LCP。

如果 height[rk[i1]]=0height[rk[i-1]]=0,那么显然成立。

如果 height[rk[i1]]>0height[rk[i-1]]>0,那么 i1i-1 和前面的后缀第一位一定相同,而第二位后的 LCP 就是 height[rk[i1]]1height[rk[i-1]]-1

又因为 i1i-1sasa 前面的那个后缀删掉最前面的字符还是一个后缀,所以 ii 与其它后缀的最大 LCP height[rk[i1]]1\geq height[rk[i-1]]-1,感性理解一下这是对的。

于是按照这个引理求就可以做到 O(n)O(n)

代码
1
2
3
4
5
6
7
8
void getheight(std::string s, int *height) {
int n = static_cast<int>(s.size()) - 1;
for (int i = 1, p = 0; i <= n; ++i) {
if (p) --p;
for (; i + p <= n && sa[rk[i] - 1] + p <= n && s[i + p] == s[sa[rk[i] - 1] + p]; ++p) {}
height[rk[i]] = p;
}
}
  • 一些应用

  1. 一个长度为 nn 的本质不同子串数是 n(n+1)2i=2nheight[i]\dfrac{n(n+1)}{2}-\sum\limits_{i=2}^{n}{height[i]}

  2. sa[i]sa[i]sa[j]sa[j] 的 LCP 是 mink=i+1jheight[k]\min\limits_{k=i+1}^{j}{height[k]}


后缀数组学习笔记
https://sobaliuziao.github.io/2023/10/02/post/e4343e5e.html
作者
Egg_laying_master
发布于
2023年10月2日
许可协议