字符串相关算法
前缀函数与 KMP 算法
前缀函数
给定一个长度为
具体实现如下:
C++
// s为下标从0开始的字符串 其长度为n
int pi[N];
pi[0] = 0;
for (int i = 1; i < n; ++i) {
int j = pi[i - 1];
while (j > 0 && s[i] != s[j]) j = pi[j - 1];
if (s[i] == s[j]) ++j;
pi[i] = j;
}
// s为下标从0开始的字符串 其长度为n
int pi[N];
pi[0] = 0;
for (int i = 1; i < n; ++i) {
int j = pi[i - 1];
while (j > 0 && s[i] != s[j]) j = pi[j - 1];
if (s[i] == s[j]) ++j;
pi[i] = j;
}
时间复杂度:
KMP 算法
经典应用:【KMP算法, Rabin-Karp算法, 快速幂】找出字符串中第一个匹配项的下标
实现一:拼接字符串,前缀表,一次匹配
详细说明见:在字符串中查找子串:Knuth–Morris–Pratt 算法
具体实现如下:
C++
// s为下标从0开始的文本串 其长度为n
// p为下标从0开始的模式串 其长度为m
char str[N + M];
int pi[N + M];
// 把字符串拼接为"p#s"的形式 其中#为一个既不在s中也不在t中出现的分隔符
strcpy(str, p);
str[m] = '#';
strcpy(str + m + 1, s);
// 求前缀函数 同时也是在匹配
for (int i = 1; i < m + 1 + n; ++i) {
int j = pi[i - 1];
while (j > 0 && str[i] != str[j]) j = pi[j - 1];
if (str[i] == str[j]) ++j;
pi[i] = j;
if (i >= m + 1 && pi[i] == m) {
// 匹配成功
}
}
// s为下标从0开始的文本串 其长度为n
// p为下标从0开始的模式串 其长度为m
char str[N + M];
int pi[N + M];
// 把字符串拼接为"p#s"的形式 其中#为一个既不在s中也不在t中出现的分隔符
strcpy(str, p);
str[m] = '#';
strcpy(str + m + 1, s);
// 求前缀函数 同时也是在匹配
for (int i = 1; i < m + 1 + n; ++i) {
int j = pi[i - 1];
while (j > 0 && str[i] != str[j]) j = pi[j - 1];
if (str[i] == str[j]) ++j;
pi[i] = j;
if (i >= m + 1 && pi[i] == m) {
// 匹配成功
}
}
时间复杂度:
实现二:前缀表,两次匹配
具体实现如下:
C++
// s为下标从0开始的文本串 其长度为n
// p为下标从0开始的模式串 其长度为m
// 求前缀表
int pi[M];
pi[0] = 0;
for (int i = 1, j = 0; i < m; ++i) {
while (j > 0 && p[i] != p[j]) j = pi[j - 1];
if (p[i] == p[j]) ++j;
pi[i] = j;
}
// 匹配
for (int i = 0, j = 0; i < n; ++i) {
while (j > 0 && s[i] != p[j]) j = pi[j - 1];
if (s[i] == p[j]) ++j;
if (j == m) {
// 匹配成功
j = pi[j - 1]; // 回退进行下一次匹配
}
}
// s为下标从0开始的文本串 其长度为n
// p为下标从0开始的模式串 其长度为m
// 求前缀表
int pi[M];
pi[0] = 0;
for (int i = 1, j = 0; i < m; ++i) {
while (j > 0 && p[i] != p[j]) j = pi[j - 1];
if (p[i] == p[j]) ++j;
pi[i] = j;
}
// 匹配
for (int i = 0, j = 0; i < n; ++i) {
while (j > 0 && s[i] != p[j]) j = pi[j - 1];
if (s[i] == p[j]) ++j;
if (j == m) {
// 匹配成功
j = pi[j - 1]; // 回退进行下一次匹配
}
}
C++
// s为下标从1开始的文本串 其长度为n
// p为下标从1开始的模式串 其长度为m
// 求前缀表
int pi[M];
pi[1] = 0;
for (int i = 2, j = 0; i <= m; ++i) {
while (j > 0 && p[i] != p[j + 1]) j = pi[j];
if (p[i] == p[j + 1]) ++j;
pi[i] = j;
}
// 匹配
for (int i = 1, j = 0; i <= n; ++i) {
while (j > 0 && s[i] != p[j + 1]) j = pi[j];
if (s[i] == p[j + 1]) ++j;
if (j == m) {
// 匹配成功
j = pi[j]; // 回退进行下一次匹配
}
}
// s为下标从1开始的文本串 其长度为n
// p为下标从1开始的模式串 其长度为m
// 求前缀表
int pi[M];
pi[1] = 0;
for (int i = 2, j = 0; i <= m; ++i) {
while (j > 0 && p[i] != p[j + 1]) j = pi[j];
if (p[i] == p[j + 1]) ++j;
pi[i] = j;
}
// 匹配
for (int i = 1, j = 0; i <= n; ++i) {
while (j > 0 && s[i] != p[j + 1]) j = pi[j];
if (s[i] == p[j + 1]) ++j;
if (j == m) {
// 匹配成功
j = pi[j]; // 回退进行下一次匹配
}
}
实现三:next 数组,两次匹配
next
数组为原版前缀表右移一位,统一减一后得到的数组。
具体实现如下:
Java
// s为下标从1开始的文本串 其长度为n
// p为下标从1开始的模式串 其长度为m
// 求next数组
int[] ne = new int[m + 1];
for (int i = 2, j = 0; i <= m; ++i) {
while (j > 0 && p[i] != p[j + 1]) j = ne[j]; // 前后缀不相同了 向前回退
if (p[i] == p[j + 1]) ++j; // 找到相同的前后缀
ne[i] = j;
}
// 匹配
for (int i = 1, j = 0; i <= n; ++i) {
while (j > 0 && s[i] != p[j + 1]) j = ne[j];
if (s[i] == p[j + 1]) ++j;
if (j == m) {
// 匹配成功
j = ne[j]; // 回退进行下一次匹配
}
}
// s为下标从1开始的文本串 其长度为n
// p为下标从1开始的模式串 其长度为m
// 求next数组
int[] ne = new int[m + 1];
for (int i = 2, j = 0; i <= m; ++i) {
while (j > 0 && p[i] != p[j + 1]) j = ne[j]; // 前后缀不相同了 向前回退
if (p[i] == p[j + 1]) ++j; // 找到相同的前后缀
ne[i] = j;
}
// 匹配
for (int i = 1, j = 0; i <= n; ++i) {
while (j > 0 && s[i] != p[j + 1]) j = ne[j];
if (s[i] == p[j + 1]) ++j;
if (j == m) {
// 匹配成功
j = ne[j]; // 回退进行下一次匹配
}
}
C++
// s为下标从1开始的文本串 其长度为n
// p为下标从1开始的模式串 其长度为m
// 求next数组
int ne[M];
for (int i = 2, j = 0; i <= m; ++i) {
while (j && p[i] != p[j + 1]) j = ne[j]; // 前后缀不相同了 向前回退
if (p[i] == p[j + 1]) ++j; // 找到相同的前后缀
ne[i] = j;
}
// 匹配
for (int i = 1, j = 0; i <= n; ++i) {
while (j && s[i] != p[j + 1]) j = ne[j];
if (s[i] == p[j + 1]) ++j;
if (j == m) {
// 匹配成功
j = ne[j]; // 回退进行下一次匹配
}
}
// s为下标从1开始的文本串 其长度为n
// p为下标从1开始的模式串 其长度为m
// 求next数组
int ne[M];
for (int i = 2, j = 0; i <= m; ++i) {
while (j && p[i] != p[j + 1]) j = ne[j]; // 前后缀不相同了 向前回退
if (p[i] == p[j + 1]) ++j; // 找到相同的前后缀
ne[i] = j;
}
// 匹配
for (int i = 1, j = 0; i <= n; ++i) {
while (j && s[i] != p[j + 1]) j = ne[j];
if (s[i] == p[j + 1]) ++j;
if (j == m) {
// 匹配成功
j = ne[j]; // 回退进行下一次匹配
}
}
Trie
最基本的 Trie 数据结构
模板题:Trie字符串统计
C++
// N为最大可能的节点数(最大可能的字符串总长度) K为字符串中可能出现的字符种类数
int sons[N][K], cnt[N], idx;
void insert(char word[]) {
int p = 0;
for (int i = 0; word[i]; ++i) {
int curr = word[i] - 'a';
if (!sons[p][curr]) sons[p][curr] = ++idx;
p = sons[p][curr];
}
++cnts[p];
}
int count(char word[]) {
int p = 0;
for (int i = 0; word[i]; ++i) {
int curr = word[i] - 'a';
if (!sons[p][curr]) return 0;
p = sons[p][curr];
}
return cnts[p];
}
// N为最大可能的节点数(最大可能的字符串总长度) K为字符串中可能出现的字符种类数
int sons[N][K], cnt[N], idx;
void insert(char word[]) {
int p = 0;
for (int i = 0; word[i]; ++i) {
int curr = word[i] - 'a';
if (!sons[p][curr]) sons[p][curr] = ++idx;
p = sons[p][curr];
}
++cnts[p];
}
int count(char word[]) {
int p = 0;
for (int i = 0; word[i]; ++i) {
int curr = word[i] - 'a';
if (!sons[p][curr]) return 0;
p = sons[p][curr];
}
return cnts[p];
}
可持久化 Trie
先放个 P4735 最大异或和 的实现代码在这,待更新...
C++
#include <cstdio>
#include <cmath>
using namespace std;
const int N = 6e5 + 10, K = log2(1e7) + 1;
int n, m;
int s[N];
char op[2];
int sons[N * K][2], vers[N * K], roots[N], idx;
void insert(int u, int p, int v) {
vers[u] = v;
for (int i = K - 1; i >= 0; --i) {
int d = (s[v] >> i) & 1;
sons[u][!d] = sons[p][!d];
sons[u][d] = ++idx;
u = sons[u][d], p = sons[p][d];
vers[u] = v;
}
}
int query(int x, int u, int l) {
int res = 0;
for (int i = K - 1; i >= 0; --i) {
int d = (x >> i) & 1;
if (vers[sons[u][!d]] >= l) res |= (1 << i), u = sons[u][!d];
else u = sons[u][d];
}
return res;
}
int main() {
scanf("%d%d", &n, &m);
vers[0] = -1;
roots[0] = ++idx;
insert(roots[0], 0, 0);
for (int i = 1; i <= n; ++i) {
scanf("%d", &s[i]);
s[i] ^= s[i - 1];
roots[i] = ++idx;
insert(roots[i], roots[i - 1], i);
}
while (m--) {
scanf("%s", op);
if (op[0] == 'A') {
scanf("%d", &s[++n]);
s[n] ^= s[n - 1];
roots[n] = ++idx;
insert(roots[n], roots[n - 1], n);
} else {
int l, r, x; scanf("%d%d%d", &l, &r, &x);
int ans = query(s[n] ^ x, roots[r - 1], l - 1);
printf("%d\n", ans);
}
}
return 0;
}
#include <cstdio>
#include <cmath>
using namespace std;
const int N = 6e5 + 10, K = log2(1e7) + 1;
int n, m;
int s[N];
char op[2];
int sons[N * K][2], vers[N * K], roots[N], idx;
void insert(int u, int p, int v) {
vers[u] = v;
for (int i = K - 1; i >= 0; --i) {
int d = (s[v] >> i) & 1;
sons[u][!d] = sons[p][!d];
sons[u][d] = ++idx;
u = sons[u][d], p = sons[p][d];
vers[u] = v;
}
}
int query(int x, int u, int l) {
int res = 0;
for (int i = K - 1; i >= 0; --i) {
int d = (x >> i) & 1;
if (vers[sons[u][!d]] >= l) res |= (1 << i), u = sons[u][!d];
else u = sons[u][d];
}
return res;
}
int main() {
scanf("%d%d", &n, &m);
vers[0] = -1;
roots[0] = ++idx;
insert(roots[0], 0, 0);
for (int i = 1; i <= n; ++i) {
scanf("%d", &s[i]);
s[i] ^= s[i - 1];
roots[i] = ++idx;
insert(roots[i], roots[i - 1], i);
}
while (m--) {
scanf("%s", op);
if (op[0] == 'A') {
scanf("%d", &s[++n]);
s[n] ^= s[n - 1];
roots[n] = ++idx;
insert(roots[n], roots[n - 1], n);
} else {
int l, r, x; scanf("%d%d%d", &l, &r, &x);
int ans = query(s[n] ^ x, roots[r - 1], l - 1);
printf("%d\n", ans);
}
}
return 0;
}
AC 自动机
AC 自动机是 以 Trie 的结构为基础,结合 KMP 的思想 建立的自动机,用于解决多模式匹配等任务。
C++
// N为最大可能的节点数(最大可能的字符串总长度) K为字符串中可能出现的字符种类数
// fail[v]表示节点v在失配时回跳边的指向的节点
// sons[u][i]表示节点u的树边或转移边指向的节点
int sons[N][K], cnts[N], idx, fail[N];
// 与Trie一样的插入
void insert(char* s) {
int p = 0;
for (int k = 0; s[k]; ++k) {
int i = s[k] - 'a';
if (!sons[p][i]) sons[p][i] = ++idx;
p = sons[p][i];
}
++cnts[p];
}
// 建立转移边和回跳边
void build() {
queue<int> que;
for (int i = 0; i < K; ++i) {
if (sons[0][i]) que.push(sons[0][i]);
}
while (!que.empty()) {
int u = que.front(); que.pop();
for (int i = 0; i < K; ++i) {
int v = sons[u][i];
// 当子节点存在时 为子节点建立回跳边: 指向父节点的回跳边所指节点的子节点
if (v) fail[v] = sons[fail[u]][i], que.push(v);
// 当子节点不存在时 为当前节点建立转移边: 指向当前节点的回跳边所指节点的子节点
else sons[u][i] = sons[fail[u]][i];
}
}
}
int query(char* s) {
int res = 0;
// u指针走主串对应节点 沿着树边/转移边走 保证不回退
for (int k = 0, u = 0; s[k]; ++k) {
u = sons[u][s[k] - 'a'];
// v指针沿着回跳边搜索模式串 每次从当前节点(u)走到根节点
for (int v = u; v && ~cnts[v]; v = fail[v]) {
// 如果模式串只算一次匹配就要置空cnts[v]和对cnts[v]做条件判断
res += cnts[v], cnts[v] = -1;
}
}
return res;
}
// N为最大可能的节点数(最大可能的字符串总长度) K为字符串中可能出现的字符种类数
// fail[v]表示节点v在失配时回跳边的指向的节点
// sons[u][i]表示节点u的树边或转移边指向的节点
int sons[N][K], cnts[N], idx, fail[N];
// 与Trie一样的插入
void insert(char* s) {
int p = 0;
for (int k = 0; s[k]; ++k) {
int i = s[k] - 'a';
if (!sons[p][i]) sons[p][i] = ++idx;
p = sons[p][i];
}
++cnts[p];
}
// 建立转移边和回跳边
void build() {
queue<int> que;
for (int i = 0; i < K; ++i) {
if (sons[0][i]) que.push(sons[0][i]);
}
while (!que.empty()) {
int u = que.front(); que.pop();
for (int i = 0; i < K; ++i) {
int v = sons[u][i];
// 当子节点存在时 为子节点建立回跳边: 指向父节点的回跳边所指节点的子节点
if (v) fail[v] = sons[fail[u]][i], que.push(v);
// 当子节点不存在时 为当前节点建立转移边: 指向当前节点的回跳边所指节点的子节点
else sons[u][i] = sons[fail[u]][i];
}
}
}
int query(char* s) {
int res = 0;
// u指针走主串对应节点 沿着树边/转移边走 保证不回退
for (int k = 0, u = 0; s[k]; ++k) {
u = sons[u][s[k] - 'a'];
// v指针沿着回跳边搜索模式串 每次从当前节点(u)走到根节点
for (int v = u; v && ~cnts[v]; v = fail[v]) {
// 如果模式串只算一次匹配就要置空cnts[v]和对cnts[v]做条件判断
res += cnts[v], cnts[v] = -1;
}
}
return res;
}
后缀数组
后缀数组(Suffix Array)主要关系到两个数组:
其中:
表示将所有后缀排序后第 小的后缀的编号,也是所说的后缀数组; 表示后缀 的排名,是重要的辅助数组。
简单来说,
这两个数组满足性质:
的做法
递增+快排。
代码中加了两个常数优化(代码中高亮部分):
- 单独提出
dup()
函数来计算是否重复:减少不连续内存访问(在数据范围较大时优化效果比较明显); - 若排名都不相同可直接生成后缀数组:考虑新的
数组,若其值域为 那么每个排名都不同,此时无需再排序。
C++
// 字符串长度为n 序列输入至s[1...n]
char s[N];
// 为了防止访问rk[sa[i]+w]与rk_bak[sa[i]+w]导致数组越界 开两倍数组
int sa[N], rk[N], rk_bak[N * 2];
bool dup(const int& x, const int& y, const int& w) {
return rk_bak[x] == rk_bak[y] && rk_bak[x + w] == rk_bak[y + w];
}
int n = strlen(s + 1);
for (int i = 1; i <= n; ++i) sa[i] = i, rk[i] = s[i];
for (int w = 1; w < n; w <<= 1) {
sort(sa + 1, sa + n + 1, [&](const int& x, const int& y) {
return rk[x] == rk[y] ? rk[x + w] < rk[y + w] : rk[x] < rk[y];
});
memcpy(rk_bak + 1, rk + 1, n * sizeof(int));
int p = 0;
for (int i = 1; i <= n; ++i) {
rk[sa[i]] = dup(sa[i], sa[i - 1], w) ? p : ++p;
}
if (p == n) break;
}
// 字符串长度为n 序列输入至s[1...n]
char s[N];
// 为了防止访问rk[sa[i]+w]与rk_bak[sa[i]+w]导致数组越界 开两倍数组
int sa[N], rk[N], rk_bak[N * 2];
bool dup(const int& x, const int& y, const int& w) {
return rk_bak[x] == rk_bak[y] && rk_bak[x + w] == rk_bak[y + w];
}
int n = strlen(s + 1);
for (int i = 1; i <= n; ++i) sa[i] = i, rk[i] = s[i];
for (int w = 1; w < n; w <<= 1) {
sort(sa + 1, sa + n + 1, [&](const int& x, const int& y) {
return rk[x] == rk[y] ? rk[x + w] < rk[y + w] : rk[x] < rk[y];
});
memcpy(rk_bak + 1, rk + 1, n * sizeof(int));
int p = 0;
for (int i = 1; i <= n; ++i) {
rk[sa[i]] = dup(sa[i], sa[i - 1], w) ? p : ++p;
}
if (p == n) break;
}
的做法
递增+计排。
更易理解的版本的代码中加了三个常数优化(代码中高亮部分):
- 单独提出
dup()
函数来计算是否重复:减少不连续内存访问(在数据范围较大时优化效果比较明显); - 若排名都不相同可直接生成后缀数组:考虑新的
数组,若其值域为 那么每个排名都不同,此时无需再排序; - 优化计数排序的值域:每次对
进行更新之后,我们都计算了一个 ,这个 即是 的值域,将值域改成它即可。
常数更小的版本的代码在上面的基础上多加了两个常数优化(代码中高亮部分):
- 第二关键字无需计数排序;
- 第二关键字排序的实质,其实就是把超出字符串范围(即
)的 放到 数组头部,然后把剩下的依原顺序放入。
C++
int idx = 0;
for (int i = n; i > n - w; --i) sk[++idx] = i;
for (int i = 1; i<= n; ++i) {
if (sa[i] > w) sk[++idx] = sa[i] - w;
}
int idx = 0;
for (int i = n; i > n - w; --i) sk[++idx] = i;
for (int i = 1; i<= n; ++i) {
if (sa[i] > w) sk[++idx] = sa[i] - w;
}
- 先把第二关键字为无穷小的位置放前面,再把剩下的按排好的顺序放进去。
- 将
存下来:减少不连续内存访问(在数据范围较大时优化效果非常明显);
C++
// 字符串长度为n 序列输入至s[1...n]
char s[N];
// 为了防止访问rk[sa[i]+w]与rk_bak[sa[i]+w]导致数组越界 开两倍数组
int sa[N], rk[N * 2], sa_bak[N], rk_bak[N * 2], cnts[N];
bool dup(const int& x, const int& y, const int& w) {
return rk_bak[x] == rk_bak[y] && rk_bak[x + w] == rk_bak[y + w];
}
int n = strlen(s + 1), m = 128;
// 计排
for (int i = 1; i <= n; ++i) ++cnts[rk[i] = s[i]];
for (int i = 1; i <= m; ++i) cnts[i] += cnts[i - 1];
for (int i = n; i >= 1; --i) sa[cnts[rk[i]]--] = i;
// 更新rk
memcpy(rk_bak + 1, rk + 1, n * sizeof(int));
for (int p = 0, i = 1; i <= n; ++i) {
rk[sa[i]] = dup(sa[i], sa[i - 1], 0) ? p : ++p;
}
for (int w = 1; w < n; w <<= 1) {
// 对第二关键字sa[i]+w进行计排
memset(cnts, 0, (m + 1) * sizeof(int));
memcpy(sa_bak + 1, sa + 1, n * sizeof(int));
for (int i = 1; i <= n; ++i) ++cnts[rk[sa_bak[i] + w]];
for (int i = 1; i <= m; ++i) cnts[i] += cnts[i - 1];
for (int i = n; i >= 1; --i) sa[cnts[rk[sa_bak[i] + w]]--] = sa_bak[i];
// 对第一关键字sa[i]进行计排
memset(cnts, 0, (m + 1) * sizeof(int));
memcpy(sa_bak + 1, sa + 1, n * sizeof(int));
for (int i = 1; i <= n; ++i) ++cnts[rk[sa_bak[i]]];
for (int i = 1; i <= m; ++i) cnts[i] += cnts[i - 1];
for (int i = n; i >= 1; --i) sa[cnts[rk[sa_bak[i]]]--] = sa_bak[i];
// 更新rk
memcpy(rk_bak + 1, rk + 1, n * sizeof(int));
int p = 0;
for (int i = 1; i <= n; ++i) {
rk[sa[i]] = dup(sa[i], sa[i - 1], w) ? p : ++p;
}
if (p == n) break;
m = p;
}
// 字符串长度为n 序列输入至s[1...n]
char s[N];
// 为了防止访问rk[sa[i]+w]与rk_bak[sa[i]+w]导致数组越界 开两倍数组
int sa[N], rk[N * 2], sa_bak[N], rk_bak[N * 2], cnts[N];
bool dup(const int& x, const int& y, const int& w) {
return rk_bak[x] == rk_bak[y] && rk_bak[x + w] == rk_bak[y + w];
}
int n = strlen(s + 1), m = 128;
// 计排
for (int i = 1; i <= n; ++i) ++cnts[rk[i] = s[i]];
for (int i = 1; i <= m; ++i) cnts[i] += cnts[i - 1];
for (int i = n; i >= 1; --i) sa[cnts[rk[i]]--] = i;
// 更新rk
memcpy(rk_bak + 1, rk + 1, n * sizeof(int));
for (int p = 0, i = 1; i <= n; ++i) {
rk[sa[i]] = dup(sa[i], sa[i - 1], 0) ? p : ++p;
}
for (int w = 1; w < n; w <<= 1) {
// 对第二关键字sa[i]+w进行计排
memset(cnts, 0, (m + 1) * sizeof(int));
memcpy(sa_bak + 1, sa + 1, n * sizeof(int));
for (int i = 1; i <= n; ++i) ++cnts[rk[sa_bak[i] + w]];
for (int i = 1; i <= m; ++i) cnts[i] += cnts[i - 1];
for (int i = n; i >= 1; --i) sa[cnts[rk[sa_bak[i] + w]]--] = sa_bak[i];
// 对第一关键字sa[i]进行计排
memset(cnts, 0, (m + 1) * sizeof(int));
memcpy(sa_bak + 1, sa + 1, n * sizeof(int));
for (int i = 1; i <= n; ++i) ++cnts[rk[sa_bak[i]]];
for (int i = 1; i <= m; ++i) cnts[i] += cnts[i - 1];
for (int i = n; i >= 1; --i) sa[cnts[rk[sa_bak[i]]]--] = sa_bak[i];
// 更新rk
memcpy(rk_bak + 1, rk + 1, n * sizeof(int));
int p = 0;
for (int i = 1; i <= n; ++i) {
rk[sa[i]] = dup(sa[i], sa[i - 1], w) ? p : ++p;
}
if (p == n) break;
m = p;
}
C++
// 字符串长度为n 序列输入至s[1...n]
char s[N];
// 为了防止访问rk[sa[i]+w]与rk_bak[sa[i]+w]导致数组越界 开两倍数组
// pk表示第一关键字(Primary Keyword) sk表示第二关键字(Secondary Keyword)
int sa[N], rk[N], rk_bak[N * 2], cnts[N], pk[N], sk[N];
bool dup(const int& x, const int& y, const int& w) {
return rk_bak[x] == rk_bak[y] && rk_bak[x + w] == rk_bak[y + w];
}
int n = strlen(s + 1), m = 128;
// 计排
for (int i = 1; i <= n; ++i) ++cnts[rk[i] = s[i]];
for (int i = 1; i <= m; ++i) cnts[i] += cnts[i - 1];
for (int i = n; i >= 1; --i) sa[cnts[rk[i]]--] = i;
for (int w = 1; w < n; w <<= 1) {
// 根据性质优化第二关键字排序
int idx = 0;
for (int i = n; i > n - w; --i) sk[++idx] = i;
for (int i = 1; i<= n; ++i) {
if (sa[i] > w) sk[++idx] = sa[i] - w;
}
// 对第一关键字sa[i]进行计排
memset(cnts, 0, (m + 1) * sizeof(int));
for (int i = 1; i <= n; ++i) ++cnts[pk[i] = rk[sk[i]]];
for (int i = 1; i <= m; ++i) cnts[i] += cnts[i - 1];
for (int i = n; i >= 1; --i) sa[cnts[pk[i]]--] = sk[i];
// 更新rk
memcpy(rk_bak + 1, rk + 1, n * sizeof(int));
int p = 0;
for (int i = 1; i <= n; ++i) {
rk[sa[i]] = dup(sa[i], sa[i - 1], w) ? p : ++p;
}
if (p == n) break;
m = p;
}
// 字符串长度为n 序列输入至s[1...n]
char s[N];
// 为了防止访问rk[sa[i]+w]与rk_bak[sa[i]+w]导致数组越界 开两倍数组
// pk表示第一关键字(Primary Keyword) sk表示第二关键字(Secondary Keyword)
int sa[N], rk[N], rk_bak[N * 2], cnts[N], pk[N], sk[N];
bool dup(const int& x, const int& y, const int& w) {
return rk_bak[x] == rk_bak[y] && rk_bak[x + w] == rk_bak[y + w];
}
int n = strlen(s + 1), m = 128;
// 计排
for (int i = 1; i <= n; ++i) ++cnts[rk[i] = s[i]];
for (int i = 1; i <= m; ++i) cnts[i] += cnts[i - 1];
for (int i = n; i >= 1; --i) sa[cnts[rk[i]]--] = i;
for (int w = 1; w < n; w <<= 1) {
// 根据性质优化第二关键字排序
int idx = 0;
for (int i = n; i > n - w; --i) sk[++idx] = i;
for (int i = 1; i<= n; ++i) {
if (sa[i] > w) sk[++idx] = sa[i] - w;
}
// 对第一关键字sa[i]进行计排
memset(cnts, 0, (m + 1) * sizeof(int));
for (int i = 1; i <= n; ++i) ++cnts[pk[i] = rk[sk[i]]];
for (int i = 1; i <= m; ++i) cnts[i] += cnts[i - 1];
for (int i = n; i >= 1; --i) sa[cnts[pk[i]]--] = sk[i];
// 更新rk
memcpy(rk_bak + 1, rk + 1, n * sizeof(int));
int p = 0;
for (int i = 1; i <= n; ++i) {
rk[sa[i]] = dup(sa[i], sa[i - 1], w) ? p : ++p;
}
if (p == n) break;
m = p;
}