后缀数组
概念讲解
什么是后缀数组?
后缀数组(Suffix Array)是将字符串的所有后缀按字典序排序后的数组。
基本概念
- SA[i]: 排名第 i 的后缀的起始位置
- Rank[i]: 从位置 i 开始的后缀的排名
- LCP[i]: 排名第 i 和第 i-1 的后缀的最长公共前缀长度
实现
倍增法
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354const int N = 1000005;
int sa[N], rank[N], height[N];
int tmp[N], cnt[N];
void buildSA(string s) {
int n = s.length();
int m = 256;
for (int i = 0; i < n; i++) {
sa[i] = i;
rank[i] = s[i];
}
for (int k = 1; k < n; k <<= 1) {
// 按第二关键字排序
memset(cnt, 0, sizeof(cnt));
for (int i = 0; i < n; i++) cnt[rank[i + k]]++;
for (int i = 1; i < m; i++) cnt[i] += cnt[i - 1];
for (int i = n - 1; i >= 0; i--) tmp[--cnt[rank[i + k]]] = i;
// 按第一关键字排序
memset(cnt, 0, sizeof(cnt));
for (int i = 0; i < n; i++) cnt[rank[tmp[i]]]++;
for (int i = 1; i < m; i++) cnt[i] += cnt[i - 1];
for (int i = n - 1; i >= 0; i--) sa[--cnt[rank[tmp[i]]]] = tmp[i];
// 重新计算 rank
tmp[sa[0]] = 0;
for (int i = 1; i < n; i++) {
tmp[sa[i]] = tmp[sa[i - 1]] +
(rank[sa[i]] != rank[sa[i - 1]] ||
rank[sa[i] + k] != rank[sa[i - 1] + k]);
}
for (int i = 0; i < n; i++) rank[i] = tmp[i];
if (rank[sa[n - 1]] == n - 1) break;
}
}
void buildHeight(string s) {
int n = s.length();
int k = 0;
for (int i = 0; i < n; i++) {
if (rank[i] == 0) continue;
int j = sa[rank[i] - 1];
while (i + k < n && j + k < n && s[i + k] == s[j + k]) k++;
height[rank[i]] = k;
if (k > 0) k--;
}
}应用场景
1. 最长公共子串
12345678910111213141516int longestCommonSubstring(string s1, string s2) {
string s = s1 + "#" + s2;
buildSA(s);
buildHeight(s);
int n = s1.length();
int result = 0;
for (int i = 1; i < s.length(); i++) {
if ((sa[i] < n) != (sa[i - 1] < n)) {
result = max(result, height[i]);
}
}
return result;
}2. 不同子串个数
12345678910111213int countDistinctSubstrings(string s) {
buildSA(s);
buildHeight(s);
int n = s.length();
int result = n * (n + 1) / 2;
for (int i = 1; i < n; i++) {
result -= height[i];
}
return result;
}考点分析
初赛考查方式
- 选择题: 后缀数组的概念
- 程序阅读: 理解后缀数组
- 编程题: 后缀数组应用
常见题型
- 最长公共子串
- 不同子串个数
- 字符串匹配
例题精讲
例题 1:最长公共子串
题目: 求两个字符串的最长公共子串。
解答:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677#include <iostream>
#include <cstring>
using namespace std;
const int N = 1000005;
int sa[N], rank[N], height[N];
int tmp[N], cnt[N];
void buildSA(string s) {
int n = s.length();
int m = 256;
for (int i = 0; i < n; i++) {
sa[i] = i;
rank[i] = s[i];
}
for (int k = 1; k < n; k <<= 1) {
memset(cnt, 0, sizeof(cnt));
for (int i = 0; i < n; i++) cnt[rank[i + k]]++;
for (int i = 1; i < m; i++) cnt[i] += cnt[i - 1];
for (int i = n - 1; i >= 0; i--) tmp[--cnt[rank[i + k]]] = i;
memset(cnt, 0, sizeof(cnt));
for (int i = 0; i < n; i++) cnt[rank[tmp[i]]]++;
for (int i = 1; i < m; i++) cnt[i] += cnt[i - 1];
for (int i = n - 1; i >= 0; i--) sa[--cnt[rank[tmp[i]]]] = tmp[i];
tmp[sa[0]] = 0;
for (int i = 1; i < n; i++) {
tmp[sa[i]] = tmp[sa[i - 1]] +
(rank[sa[i]] != rank[sa[i - 1]] ||
rank[sa[i] + k] != rank[sa[i - 1] + k]);
}
for (int i = 0; i < n; i++) rank[i] = tmp[i];
if (rank[sa[n - 1]] == n - 1) break;
}
}
void buildHeight(string s) {
int n = s.length();
int k = 0;
for (int i = 0; i < n; i++) {
if (rank[i] == 0) continue;
int j = sa[rank[i] - 1];
while (i + k < n && j + k < n && s[i + k] == s[j + k]) k++;
height[rank[i]] = k;
if (k > 0) k--;
}
}
int main() {
string s1, s2;
cin >> s1 >> s2;
string s = s1 + "#" + s2;
buildSA(s);
buildHeight(s);
int n = s1.length();
int result = 0;
for (int i = 1; i < s.length(); i++) {
if ((sa[i] < n) != (sa[i - 1] < n)) {
result = max(result, height[i]);
}
}
cout << result << endl;
return 0;
}练习题
- 洛谷 P3809 后缀排序
查看答案
思路:后缀数组模板题。
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152#include <iostream>
#include <cstring>
using namespace std;
const int N = 1000005;
int sa[N], rank[N], tmp[N], cnt[N];
void buildSA(string s) {
int n = s.length();
int m = 256;
for (int i = 0; i < n; i++) {
sa[i] = i;
rank[i] = s[i];
}
for (int k = 1; k < n; k <<= 1) {
memset(cnt, 0, sizeof(cnt));
for (int i = 0; i < n; i++) cnt[rank[i + k]]++;
for (int i = 1; i < m; i++) cnt[i] += cnt[i - 1];
for (int i = n - 1; i >= 0; i--) tmp[--cnt[rank[i + k]]] = i;
memset(cnt, 0, sizeof(cnt));
for (int i = 0; i < n; i++) cnt[rank[tmp[i]]]++;
for (int i = 1; i < m; i++) cnt[i] += cnt[i - 1];
for (int i = n - 1; i >= 0; i--) sa[--cnt[rank[tmp[i]]]] = tmp[i];
tmp[sa[0]] = 0;
for (int i = 1; i < n; i++) {
tmp[sa[i]] = tmp[sa[i - 1]] +
(rank[sa[i]] != rank[sa[i - 1]] ||
rank[sa[i] + k] != rank[sa[i - 1] + k]);
}
for (int i = 0; i < n; i++) rank[i] = tmp[i];
if (rank[sa[n - 1]] == n - 1) break;
}
}
int main() {
string s;
cin >> s;
buildSA(s);
for (int i = 0; i < s.length(); i++) {
cout << sa[i] + 1 << " ";
}
cout << endl;
return 0;
}- 洛谷 P2408 不同子串个数
查看答案
思路:后缀数组,不同子串个数 = 总子串数 - LCP 之和。
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273#include <iostream>
#include <cstring>
using namespace std;
const int N = 1000005;
int sa[N], rank[N], height[N], tmp[N], cnt[N];
void buildSA(string s) {
int n = s.length();
int m = 256;
for (int i = 0; i < n; i++) {
sa[i] = i;
rank[i] = s[i];
}
for (int k = 1; k < n; k <<= 1) {
memset(cnt, 0, sizeof(cnt));
for (int i = 0; i < n; i++) cnt[rank[i + k]]++;
for (int i = 1; i < m; i++) cnt[i] += cnt[i - 1];
for (int i = n - 1; i >= 0; i--) tmp[--cnt[rank[i + k]]] = i;
memset(cnt, 0, sizeof(cnt));
for (int i = 0; i < n; i++) cnt[rank[tmp[i]]]++;
for (int i = 1; i < m; i++) cnt[i] += cnt[i - 1];
for (int i = n - 1; i >= 0; i--) sa[--cnt[rank[tmp[i]]]] = tmp[i];
tmp[sa[0]] = 0;
for (int i = 1; i < n; i++) {
tmp[sa[i]] = tmp[sa[i - 1]] +
(rank[sa[i]] != rank[sa[i - 1]] ||
rank[sa[i] + k] != rank[sa[i - 1] + k]);
}
for (int i = 0; i < n; i++) rank[i] = tmp[i];
if (rank[sa[n - 1]] == n - 1) break;
}
}
void buildHeight(string s) {
int n = s.length();
int k = 0;
for (int i = 0; i < n; i++) {
if (rank[i] == 0) continue;
int j = sa[rank[i] - 1];
while (i + k < n && j + k < n && s[i + k] == s[j + k]) k++;
height[rank[i]] = k;
if (k > 0) k--;
}
}
int main() {
int n;
cin >> n;
string s;
cin >> s;
buildSA(s);
buildHeight(s);
long long result = (long long)n * (n + 1) / 2;
for (int i = 1; i < n; i++) {
result -= height[i];
}
cout << result << endl;
return 0;
}- 洛谷 P2852 [USACO06DEC] Milk Patterns G
查看答案
思路:后缀数组 + 二分答案。
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091#include <iostream>
#include <cstring>
using namespace std;
const int N = 20005;
int sa[N], rank[N], height[N], tmp[N], cnt[N];
int a[N];
void buildSA(int n, int m) {
for (int i = 0; i < n; i++) {
sa[i] = i;
rank[i] = a[i];
}
for (int k = 1; k < n; k <<= 1) {
memset(cnt, 0, sizeof(cnt));
for (int i = 0; i < n; i++) cnt[rank[i + k]]++;
for (int i = 1; i < m; i++) cnt[i] += cnt[i - 1];
for (int i = n - 1; i >= 0; i--) tmp[--cnt[rank[i + k]]] = i;
memset(cnt, 0, sizeof(cnt));
for (int i = 0; i < n; i++) cnt[rank[tmp[i]]]++;
for (int i = 1; i < m; i++) cnt[i] += cnt[i - 1];
for (int i = n - 1; i >= 0; i--) sa[--cnt[rank[tmp[i]]]] = tmp[i];
tmp[sa[0]] = 0;
for (int i = 1; i < n; i++) {
tmp[sa[i]] = tmp[sa[i - 1]] +
(rank[sa[i]] != rank[sa[i - 1]] ||
rank[sa[i] + k] != rank[sa[i - 1] + k]);
}
for (int i = 0; i < n; i++) rank[i] = tmp[i];
if (rank[sa[n - 1]] == n - 1) break;
}
}
void buildHeight(int n) {
int k = 0;
for (int i = 0; i < n; i++) {
if (rank[i] == 0) continue;
int j = sa[rank[i] - 1];
while (i + k < n && j + k < n && a[i + k] == a[j + k]) k++;
height[rank[i]] = k;
if (k > 0) k--;
}
}
bool check(int n, int k, int mid) {
int count = 1;
for (int i = 1; i < n; i++) {
if (height[i] >= mid) {
count++;
if (count >= k) return true;
} else {
count = 1;
}
}
return false;
}
int main() {
int n, k;
cin >> n >> k;
int maxVal = 0;
for (int i = 0; i < n; i++) {
cin >> a[i];
maxVal = max(maxVal, a[i]);
}
buildSA(n, maxVal + 1);
buildHeight(n);
int left = 0, right = n;
while (left < right) {
int mid = (left + right + 1) / 2;
if (check(n, k, mid)) {
left = mid;
} else {
right = mid - 1;
}
}
cout << left << endl;
return 0;
}易错点
⚠️ 注意:
- 后缀数组的构造是关键
- LCP 数组有很多应用
- 后缀数组是字符串算法的高级内容
- 注意数组大小和边界