后缀数组简介
一些约定¶
字符串相关的定义请参考 字符串基础。
字符串下标从
" 后缀
后缀数组是什么?¶
后缀数组(Suffix Array)主要是两个数组:
其中,
这两个数组满足性质:
后缀数组示例:
后缀数组怎么求?¶
O(n^2logn) 做法¶
我相信这个做法大家还是能自己想到的,用 string
+sort
就可以了。由于比较两个字符串是
O(nlog^2n) 做法¶
这个做法要用到倍增的思想。
先对每个长度为
假设我们已经知道了长度为
倍增排序示意图:
如果用 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 | #include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>
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;
}
|
O(nlogn) 做法¶
在刚刚的
由于计算后缀数组的过程中排序的关键字是排名,值域为
参考代码
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 | #include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
const int N = 1000010;
char s[N];
int n, sa[N], rk[N << 1], oldrk[N << 1], id[N], cnt[N];
int main() {
int i, m, p, w;
scanf("%s", s + 1);
n = strlen(s + 1);
m = max(n, 300);
for (i = 1; i <= n; ++i) ++cnt[rk[i] = s[i]];
for (i = 1; i <= m; ++i) cnt[i] += cnt[i - 1];
for (i = n; i >= 1; --i) sa[cnt[rk[i]]--] = i;
for (w = 1; w < n; w <<= 1) {
memset(cnt, 0, sizeof(cnt));
for (i = 1; i <= n; ++i) id[i] = sa[i];
for (i = 1; i <= n; ++i) ++cnt[rk[id[i] + w]];
for (i = 1; i <= m; ++i) cnt[i] += cnt[i - 1];
for (i = n; i >= 1; --i) sa[cnt[rk[id[i] + w]]--] = id[i];
memset(cnt, 0, sizeof(cnt));
for (i = 1; i <= n; ++i) id[i] = sa[i];
for (i = 1; i <= n; ++i) ++cnt[rk[id[i]]];
for (i = 1; i <= m; ++i) cnt[i] += cnt[i - 1];
for (i = n; i >= 1; --i) sa[cnt[rk[id[i]]]--] = id[i];
memcpy(oldrk, rk, sizeof(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;
}
}
}
for (i = 1; i <= n; ++i) printf("%d ", sa[i]);
return 0;
}
|
一些常数优化¶
如果你把上面那份代码交到 LOJ #111: 后缀排序 上:
这是因为,上面那份代码的常数的确很大。
第二关键字无需计数排序¶
实际上,像这样就可以了:
1 2 3 4 | for (p = 0, i = n; i > n - w; --i) id[++p] = i;
for (i = 1; i <= n; ++i) {
if (sa[i] > w) id[++p] = sa[i] - w;
}
|
意会一下,先把
优化计数排序的值域¶
每次对
将 rk[id[i]] 存下来,减少不连续内存访问¶
这个优化在数据范围较大时效果非常明显。
用函数 cmp 来计算是否重复¶
同样是减少不连续内存访问,在数据范围较大时效果比较明显。
把 oldrk[sa[i]] == oldrk[sa[i - 1]] && oldrk[sa[i] + w] == oldrk[sa[i - 1] + w]
替换成 cmp(sa[i], sa[i - 1], w)
,bool cmp(int x, int y, int w) { return oldrk[x] == oldrk[y] && oldrk[x + w] == oldrk[y + w]; }
。
若排名都不相同可直接生成后缀数组¶
考虑新的
参考代码
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 | #include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
const int N = 1000010;
char s[N];
int n, sa[N], rk[N], oldrk[N << 1], id[N], px[N], cnt[N];
// px[i] = rk[id[i]](用于排序的数组所以叫 px)
bool cmp(int x, int y, int w) {
return oldrk[x] == oldrk[y] && oldrk[x + w] == oldrk[y + w];
}
int main() {
int i, m = 300, p, w;
scanf("%s", s + 1);
n = strlen(s + 1);
for (i = 1; i <= n; ++i) ++cnt[rk[i] = s[i]];
for (i = 1; i <= m; ++i) cnt[i] += cnt[i - 1];
for (i = n; i >= 1; --i) sa[cnt[rk[i]]--] = i;
for (w = 1;; w <<= 1, m = p) { // m=p 就是优化计数排序值域
for (p = 0, i = n; i > n - w; --i) id[++p] = i;
for (i = 1; i <= n; ++i)
if (sa[i] > w) id[++p] = sa[i] - w;
memset(cnt, 0, sizeof(cnt));
for (i = 1; i <= n; ++i) ++cnt[px[i] = rk[id[i]]];
for (i = 1; i <= m; ++i) cnt[i] += cnt[i - 1];
for (i = n; i >= 1; --i) sa[cnt[px[i]]--] = id[i];
memcpy(oldrk, rk, sizeof(rk));
for (p = 0, i = 1; i <= n; ++i)
rk[sa[i]] = cmp(sa[i], sa[i - 1], w) ? p : ++p;
if (p == n) {
for (int i = 1; i <= n; ++i) sa[rk[i]] = i;
break;
}
}
for (i = 1; i <= n; ++i) printf("%d ", sa[i]);
return 0;
}
|
O(n) 做法¶
在一般的题目中,常数较小的倍增求后缀数组是完全够用的,求后缀数组以外的部分也经常有
但如果遇到特殊题目、时限较紧的题目,或者是你想追求更短的用时,就需要学习
SA-IS¶
可以参考 诱导排序与 SA-IS 算法。
DC3¶
可以参考[2009]后缀数组——处理字符串的有力工具 by. 罗穗骞。
后缀数组的应用¶
寻找最小的循环移动位置¶
将字符串
例题:「JSOI2007」字符加密。
在字符串中找子串¶
任务是在线地在主串
从字符串首尾取字符最小化字典序¶
题意:给你一个字符串,每次从首或尾取一个字符组成字符串,问所有能够组成的字符串中字典序最小的一个。
题解
暴力做法就是每次最坏
由于需要在原串后缀与反串后缀构成的集合内比较大小,可以将反串拼接在原串后,并在中间加上一个没出现过的字符(如 #
,代码中可以直接使用空字符),求后缀数组,即可
参考代码
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 | #include <cctype>
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
const int N = 1000010;
char s[N];
int n, sa[N], id[N], oldrk[N * 2], rk[N * 2], px[N], cnt[N];
bool cmp(int x, int y, int w) {
return oldrk[x] == oldrk[y] && oldrk[x + w] == oldrk[y + w];
}
int main() {
int i, w, m = 200, p, l = 1, r, tot = 0;
cin >> n;
r = n;
for (i = 1; i <= n; ++i)
while (!isalpha(s[i] = getchar()))
;
for (i = 1; i <= n; ++i)
rk[i] = rk[2 * n + 2 - i] = s[i]; //拼接正反两个字符串,中间空出一个字符
n = 2 * n + 1;
//求后缀数组
for (i = 1; i <= n; ++i) ++cnt[rk[i]];
for (i = 1; i <= m; ++i) cnt[i] += cnt[i - 1];
for (i = n; i >= 1; --i) sa[cnt[rk[i]]--] = i;
for (w = 1; w < n; w *= 2, m = p) { // m=p 就是优化计数排序值域
for (p = 0, i = n; i > n - w; --i) id[++p] = i;
for (i = 1; i <= n; ++i)
if (sa[i] > w) id[++p] = sa[i] - w;
memset(cnt, 0, sizeof(cnt));
for (i = 1; i <= n; ++i) ++cnt[px[i] = rk[id[i]]];
for (i = 1; i <= m; ++i) cnt[i] += cnt[i - 1];
for (i = n; i >= 1; --i) sa[cnt[px[i]]--] = id[i];
memcpy(oldrk, rk, sizeof(rk));
for (p = 0, i = 1; i <= n; ++i)
rk[sa[i]] = cmp(sa[i], sa[i - 1], w) ? p : ++p;
}
//利用后缀数组O(1)进行判断
while (l <= r) {
printf("%c", rk[l] < rk[n + 1 - r] ? s[l++] : s[r--]);
if ((++tot) % 80 == 0) puts(""); //回车
}
return 0;
}
|
height 数组¶
LCP(最长公共前缀)¶
两个字符串
下文中以
height 数组的定义¶
O(n) 求 height 数组需要的一个引理¶
证明:
当
当
设后缀
简单来说:
O(n) 求 height 数组的代码实现¶
利用上面这个引理暴力求即可:
1 2 3 4 5 | for (i = 1, k = 0; i <= n; ++i) {
if (k) --k;
while (s[i + k] == s[sa[rk[i] - 1] + k]) ++k;
ht[rk[i]] = k; // height太长了缩写为ht
}
|
height 数组的应用¶
两子串最长公共前缀¶
感性理解:如果
严格证明可以参考[2004]后缀数组 by. 徐智磊。
有了这个定理,求两子串最长公共前缀就转化为了 RMQ 问题。
比较一个字符串的两个子串的大小关系¶
假设需要比较的是
若
否则,
不同子串的数目¶
子串就是后缀的前缀,所以可以枚举每个后缀,计算前缀总数,再减掉重复。
“前缀总数”其实就是子串个数,为
如果按后缀排序的顺序枚举后缀,每次新增的子串就是除了与上一个后缀的 LCP 剩下的前缀。这些前缀一定是新增的,否则会破坏
所以答案为:
出现至少 k 次的子串的最大长度¶
题解
出现至少
所以,求出每相邻
可以使用单调队列
参考代码
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 56 | #include <cstdio>
#include <cstring>
#include <iostream>
#include <set>
using namespace std;
const int N = 40010;
int n, k, a[N], sa[N], rk[N], oldrk[N], id[N], px[N], cnt[1000010], ht[N], ans;
multiset<int> t; // multiset 是最好写的实现方式
bool cmp(int x, int y, int w) {
return oldrk[x] == oldrk[y] && oldrk[x + w] == oldrk[y + w];
}
int main() {
int i, j, w, p, m = 1000000;
scanf("%d%d", &n, &k);
--k;
for (i = 1; i <= n; ++i) scanf("%d", a + i); //求后缀数组
for (i = 1; i <= n; ++i) ++cnt[rk[i] = a[i]];
for (i = 1; i <= m; ++i) cnt[i] += cnt[i - 1];
for (i = n; i >= 1; --i) sa[cnt[rk[i]]--] = i;
for (w = 1; w < n; w <<= 1, m = p) {
for (p = 0, i = n; i > n - w; --i) id[++p] = i;
for (i = 1; i <= n; ++i)
if (sa[i] > w) id[++p] = sa[i] - w;
memset(cnt, 0, sizeof(cnt));
for (i = 1; i <= n; ++i) ++cnt[px[i] = rk[id[i]]];
for (i = 1; i <= m; ++i) cnt[i] += cnt[i - 1];
for (i = n; i >= 1; --i) sa[cnt[px[i]]--] = id[i];
memcpy(oldrk, rk, sizeof(rk));
for (p = 0, i = 1; i <= n; ++i)
rk[sa[i]] = cmp(sa[i], sa[i - 1], w) ? p : ++p;
}
for (i = 1, j = 0; i <= n; ++i) { //求 height
if (j) --j;
while (a[i + j] == a[sa[rk[i] - 1] + j]) ++j;
ht[rk[i]] = j;
}
for (i = 1; i <= n; ++i) { //求所有最小值的最大值
t.insert(ht[i]);
if (i > k) t.erase(t.find(ht[i - k]));
ans = max(ans, *t.begin());
}
cout << ans;
return 0;
}
|
是否有某字符串在文本串中至少不重叠地出现了两次¶
可以二分目标串的长度
连续的若干个相同子串¶
我们可以枚举连续串的长度
结合并查集¶
某些题目求解时要求你将后缀数组划分成若干个连续 LCP 长度大于等于某一值的段,亦即将
经典题目:「NOI2015」品酒大会
结合线段树¶
某些题目让你求满足条件的前若干个数,而这些数又在后缀排序中的一个区间内。这时我们可以用归并排序的性质来合并两个结点的信息,利用线段树维护和查询区间答案。
结合单调栈¶
例题:「AHOI2013」差异
题解
被加数的前两项很好处理,为
我们知道
考虑每个位置对答案的贡献是哪些后缀的 LCP,其实就是从它开始向左若干个连续的
单调栈部分类似于 Luogu P2659 美丽的序列 以及 悬线法。
参考代码
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 56 57 58 59 60 61 62 63 64 65 | #include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
const int N = 500010;
char s[N];
int n, sa[N], rk[N << 1], oldrk[N << 1], id[N], px[N], cnt[N], ht[N], sta[N],
top, l[N];
long long ans;
bool cmp(int x, int y, int w) {
return oldrk[x] == oldrk[y] && oldrk[x + w] == oldrk[y + w];
}
int main() {
int i, k, w, p, m = 300;
scanf("%s", s + 1);
n = strlen(s + 1);
ans = 1ll * n * (n - 1) * (n + 1) / 2;
//求后缀数组
for (i = 1; i <= n; ++i) ++cnt[rk[i] = s[i]];
for (i = 1; i <= m; ++i) cnt[i] += cnt[i - 1];
for (i = n; i >= 1; --i) sa[cnt[rk[i]]--] = i;
for (w = 1; w < n; w <<= 1, m = p) {
for (p = 0, i = n; i > n - w; --i) id[++p] = i;
for (i = 1; i <= n; ++i)
if (sa[i] > w) id[++p] = sa[i] - w;
memset(cnt, 0, sizeof(cnt));
for (i = 1; i <= n; ++i) ++cnt[px[i] = rk[id[i]]];
for (i = 1; i <= m; ++i) cnt[i] += cnt[i - 1];
for (i = n; i >= 1; --i) sa[cnt[px[i]]--] = id[i];
memcpy(oldrk, rk, sizeof(rk));
for (p = 0, i = 1; i <= n; ++i)
rk[sa[i]] = cmp(sa[i], sa[i - 1], w) ? p : ++p;
}
//求 height
for (i = 1, k = 0; i <= n; ++i) {
if (k) --k;
while (s[i + k] == s[sa[rk[i] - 1] + k]) ++k;
ht[rk[i]] = k;
}
//维护单调栈
for (i = 1; i <= n; ++i) {
while (ht[sta[top]] > ht[i]) --top; // top类似于一个指针
l[i] = i - sta[top];
sta[++top] = i;
}
//最后利用单调栈算 ans
sta[++top] = n + 1;
ht[n + 1] = -1;
for (i = n; i >= 1; --i) {
while (ht[sta[top]] >= ht[i]) --top;
ans -= 2ll * ht[i] * l[i] * (sta[top] - i);
sta[++top] = i;
}
cout << ans;
return 0;
}
|
类似的题目:「HAOI2016」找相同字符。
习题¶
- Uva 760 - DNA Sequencing
- Uva 1223 - Editor
- Codechef - Tandem
- Codechef - Substrings and Repetitions
- Codechef - Entangled Strings
- Codeforces - Martian Strings
- Codeforces - Little Elephant and Strings
- SPOJ - Ada and Terramorphing
- SPOJ - Ada and Substring
- UVA - 1227 - The longest constant gene
- SPOJ - Longest Common Substring
- UVA 11512 - GATTACA
- LA 7502 - Suffixes and Palindromes
- GYM - Por Costel and the Censorship Committee
- UVA 1254 - Top 10
- UVA 12191 - File Recover
- UVA 12206 - Stammering Aliens
- Codechef - Jarvis and LCP
- LA 3943 - Liking's Letter
- UVA 11107 - Life Forms
- UVA 12974 - Exquisite Strings
- UVA 10526 - Intellectual Property
- UVA 12338 - Anti-Rhyme Pairs
- DevSkills Reconstructing Blue Print of Life
- UVA 12191 - File Recover
- SPOJ - Suffix Array
- LA 4513 - Stammering Aliens
- SPOJ - LCS2
- Codeforces - Fake News (hard)
- SPOJ - Longest Commong Substring
- SPOJ - Lexicographical Substring Search
- Codeforces - Forbidden Indices
- Codeforces - Tricky and Clever Password
- LA 6856 - Circle of digits
参考资料¶
本页面中(4070a9b 引入的部分)主要译自博文 Суффиксный массив 与其英文翻译版 Suffix Array。其中俄文版版权协议为 Public Domain + Leave a Link;英文版版权协议为 CC-BY-SA 4.0。
论文:
build本页面最近更新:,更新历史
edit发现错误?想一起完善? 在 GitHub 上编辑此页!
people本页面贡献者:OI-wiki
copyright本页面的全部内容在 CC BY-SA 4.0 和 SATA 协议之条款下提供,附加条款亦可能应用