#include<cstdio>#include<cstring>#include<algorithm>usingnamespace std;
constint maxn = 1e6 + 5;
constint maxv = 2e6 + 5;
int n, m;
int sa[maxn], id[maxn];
int cnt[maxn], rk[maxv], oldrk[maxv];
char s[maxn];
intmain(){
// freopen("P3809_8.in", "r", stdin);// freopen("P3809_8.out", "w", stdout);scanf("%s", s + 1);
n = strlen(s + 1);
m = max(n, 300);
for (int i = 1; i <= n; i++)
cnt[rk[i] = s[i]]++;
for (int i = 1; i <= m; i++)
cnt[i] += cnt[i - 1];
for (int i = n; i >= 1; i--)
sa[cnt[rk[i]]--] = i;
for (int j = 1; j < n; j <<= 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] + j]]++;
for (int i = 1; i <= m; i++)
cnt[i] += cnt[i - 1];
for (int i = n; i >= 1; i--)
sa[cnt[rk[id[i] + j]]--] = 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 >= 1; i--)
sa[cnt[rk[id[i]]]--] = id[i];
memcpy(oldrk, rk, sizeof(rk));
for (int p = 0, i = 1; i <= n; i++)
{
if (oldrk[sa[i]] == oldrk[sa[i - 1]] && oldrk[sa[i] + j] == oldrk[sa[i - 1] + j])
rk[sa[i]] = p;
else
rk[sa[i]] = ++p;
}
}
for (int i = 1; i <= n; i++)
printf("%d ", sa[i]);
return0;
}
常数优化
由于 本人的代码常数大 倍增法实现的 SA 常数较大,您会发现如果将裸的模板交到洛谷 P3809 可以 AC,但是在 LOJ#111 上大概率会 TLE。因此本文将介绍一些非常常见的卡常方法来帮助您卡过 SA 模板。亲测洛谷无常数优化与常数优化代码用时相差 4 到 5 倍。
基数排序
我们发现实际上第二关键字无需进行基数排序,原因是我们在代码中仅仅使用第二关键字统计了每一个排名的出现次数,所以关键字出现的顺序并无太大影响。我们可以先把 i+j>n 的所有 i 记录下来,然后再遍历一遍数组,∀1≤i≤n,sai>j,记录下 sai−j>0。实现可以是这样的:
for (int i = n; i > n - j; i--)
id[++p] = i;
for (int i = 1; i <= n; i++)
if (sa[i] > j)
id[++p] = sa[i] - j;
优化值域
我们在离散化的过程中记录了累加变量 p 表示当前元素的离散化下标,那么每次排名的取值范围显然在 [1,p]。我们每次排序后令 m=p,从而优化枚举的值域。
优化不连续内存访问
我们在离散化的过程中会访问不连续的内存,这里可以用函数来判断:
boolcmp(int x, int y, int w){
return oldrk[x] == oldrk[y] && oldrk[x + w] == oldrk[y + w];
}
结束条件
如果每个后缀的排名都不一样说明后缀排序结束,那么此时的排名取值范围在 [1,n] 之间,判断离散化下标是否等于 n 即可,是则直接退出。
#include<cstdio>#include<cstring>#include<algorithm>usingnamespace std;
constint maxn = 1e6 + 5;
constint maxm = 2e6 + 5;
int n, m;
int rk[maxm], oldrk[maxm];
int sa[maxn], id[maxn], cnt[maxn];
char s[maxn];
boolcmp(int x, int y, int w){
return oldrk[x] == oldrk[y] && oldrk[x + w] == oldrk[y + w];
}
intmain(){
int p = 0;
scanf("%s", s + 1);
// m 为基数排序的值域上限// 理论上因为字符串 S 有 n 个后缀// 所以后缀 i 在所有后缀中的字典序排名值域为 [1, n]// 但是初始时我们用字符的 ASCII 码代表其排名// 所以 m 的初始值应大于最大的可能出现的字符的 ASCII 码值// 这里取 300,改为 114514 等数字均可
n = strlen(s + 1), m = 300;
for (int i = 1; i <= n; i++)
cnt[rk[i] = s[i]]++; // 等价于 rk[i] = s[i], cnt[rk[i]]++ for (int i = 1; i <= m; i++)
cnt[i] += cnt[i - 1];
for (int i = n; i >= 1; i--)
sa[cnt[rk[i]]--] = i; // 第一轮后缀排序 for (int j = 1; ; j <<= 1, m = p, p = 0)
{
for (int i = n; i > n - j; i--)
id[++p] = i;
for (int i = 1; i <= n; i++)
if (sa[i] > j)
id[++p] = sa[i] - j;
// 上面是是经过优化的代码,等价于下面这段代码// 作用是以后缀 id[i] + j 的排名为第二关键字进行基数排序 // 后面要用 id 来更新 sa,因此不能直接用 sa 数组 // 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] + j]]++;// for (int i = 1; i <= m; i++)// cnt[i] += cnt[i - 1];// for (int i = n; i >= 1; i--)// sa[cnt[rk[id[i] + j]]--] = id[i]; memset(cnt, 0, (m + 1) * sizeof(int));
// 按后缀 id[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 >= 1; i--)
sa[cnt[rk[id[i]]]--] = id[i];
// 对 rk 的排序结果离散化 memcpy(oldrk, rk, sizeof(rk)), p = 0;
for (int i = 1; i <= n; i++)
rk[sa[i]] = cmp(sa[i], sa[i - 1], j) ? p : ++p;
if (p == n)
{
// 根据 sa[rk[i]] = i 来更新 sa for (int i = 1; i <= n; i++)
sa[rk[i]] = i;
break;
}
}
for (int i = 1; i <= n; i++)
printf("%d ", sa[i]);
return0;
}
LCP
基本概念
LCP(LongestCommonPrefix),即最长公共前缀。在后缀数组一类的问题中,LCP(i,j) 定义为后缀 sai 和后缀 saj 的最长公共前缀。利用 LCP 及其性质可以方便地解决很多问题。
voidget_height(){
int k = 0;
for (int i = 1; i <= n; i++)
rk[sa[i]] = i;
for (int i = 1; i <= n; i++)
{
if (rk[i] == 1)
continue;
if (k) k--;
int j = sa[rk[i] - 1];
while (i + k <= n && j + k <= n && s[i + k] == s[j + k]) k++;
height[rk[i]] = k;
}
}
回到本题。我们发现如果情况够好,区间 [x,y] 和区间 [l,r] 的 LCP 长度应该等于后缀 x 和后缀 l 的 LCP 长度。但是由于后缀 x 和后缀 l 的 LCP 可能会超过给出的区间,因此答案还需要和两个区间的长度取较小值。
参考代码
#include<cstdio>#include<cstring>#include<iostream>#include<algorithm>usingnamespace std;
constint maxn = 1e5 + 5;
constint maxm = 2e5 + 5;
int n, m, q;
int rk[maxm], oldrk[maxm];
int height[maxn], f[maxn][20];
int sa[maxn], cnt[maxn], id[maxn];
char s[maxn];
boolcmp(int x, int y, int w){
return oldrk[x] == oldrk[y] && oldrk[x + w] == oldrk[y + w];
}
voidget_height(){
int k = 0;
for (int i = 1; i <= n; i++)
rk[sa[i]] = i;
for (int i = 1; i <= n; i++)
{
if (rk[i] == 1)
continue;
if (k) k--;
int j = sa[rk[i] - 1];
while (i + k <= n && j + k <= n && s[i + k] == s[j + k]) k++;
height[rk[i]] = k;
}
}
intget_log(int x){
int num = 1, res = 0;
while (num <= x)
{
num <<= 1;
res++;
}
return --res;
}
intquery(int x, int y){
int len = get_log(y - x + 1);
returnmin(f[x][len], f[y - (1 << len) + 1][len]);
}
intmain(){
int x, y, l, r;
int ans, p = 0;
scanf("%s", s + 1);
n = strlen(s + 1), m = 300;
for (int i = 1; i <= n; i++)
cnt[rk[i] = s[i]]++;
for (int i = 1; i <= m; i++)
cnt[i] += cnt[i - 1];
for (int i = n; i >= 1; i--)
sa[cnt[rk[i]]--] = i;
for (int j = 1; ; j <<= 1, m = p, p = 0)
{
for (int i = n; i > n - j; i--)
id[++p] = i;
for (int i = 1; i <= n; i++)
if (sa[i] > j)
id[++p] = sa[i] - j;
memset(cnt, 0, (m + 1) * sizeof(int));
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 >= 1; i--)
sa[cnt[rk[id[i]]]--] = id[i];
memcpy(oldrk, rk, sizeof(rk)), p = 0;
for (int i = 1; i <= n; i++)
rk[sa[i]] = cmp(sa[i], sa[i - 1], j) ? p : ++p;
if (p == n)
{
for (int i = 1; i <= n; i++)
sa[rk[i]] = i;
break;
}
}
get_height();
for (int i = 1; i <= n; i++)
f[i][0] = height[i];
for (int j = 1; (1 << j) <= n; j++)
for (int i = 1; i + (1 << j) - 1 <= n; i++)
f[i][j] = min(f[i][j - 1], f[i + (1 << (j - 1))][j - 1]);
scanf("%d", &q);
for (int i = 1; i <= q; i++)
{
scanf("%d%d%d%d", &x, &y, &l, &r);
if (x == l)
{
printf("%d\n", min(y, r) - x + 1);
continue;
}
int rkx = rk[x], rkl = rk[l];
if (rkx > rkl)
swap(l, x), swap(r, y), swap(rkx, rkl);
ans = min(min(y - x + 1, r - l + 1), query(rkx + 1, rkl));
printf("%d\n", ans);
}
return0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 用 .NET NativeAOT 构建完全 distroless 的静态链接应用
· 为什么构造函数需要尽可能的简单
· 探秘 MySQL 索引底层原理,解锁数据库优化的关键密码(下)
· 大模型 Token 究竟是啥:图解大模型Token
· 35岁程序员的中年求职记:四次碰壁后的深度反思
· 用 .NET NativeAOT 构建完全 distroless 的静态链接应用
· 如何开发 MCP 服务?保姆级教程!
· 1.net core 工作流WorkFlow流程(介绍)
· C# 工业视觉开发必刷20道 Halcon 面试题
· 从零散笔记到结构化知识库:我的文档网站建设之路