后缀数组学习笔记
前言
后缀数组(Suffix Array,简称 SA)是一种解决某些字符串问题的常用工具。解决这些字符串问题时,经常用后缀数组对问题进行一定的转化成其它的模型,然后套用那个模型的解决方法加以解决原问题。
约定
本文做以下约定:
-
本文撰写时间跨度较大,有些符号会出现正体、斜体混用的情况,请读者甄别。
-
记
为字符集。具体的字符(串)使用⌈打印机字体⌋表示,如 。用 表示字符串 的长度。本文中字符串的下标从 开始,代码中视实现方便程度可能会有所差异。 -
记
表示字符 从左往右依次拼接形成的字符串。记 为字符串 从左至右的第 个字符。若 ,则认为它为空字符。 -
记
为字符串 的最长公共前缀。形式化地, ,当且仅当 且 。下文有时会简称为 。 -
称一个字符串
的字典序比字符串 小,记 ,当且仅当 且 。认为空字符的字典序极小。 -
记
为字符串 删去前 个字母和后 个字母得到的字符串,称其为字符串 从 到 的子串。形式化地, 。显然, 为字符串 的一个前缀, 为字符串 的一个后缀。 -
对于字符串
,记 , 。 -
记
表示将字符串 的所有后缀按照字典序从小到大排序后,后缀 的排在第几位。称为后缀 的排名。 -
记
表示排名为 的后缀的起始位置。形式化地,若 ,则 ,即 。
构建
后缀数组最初被用来解决这样一个问题:
给出一个字符串
,对于 ,求 。
。
表面上这题要求我们求
【方法一:取出所有后缀并进行排序】
这就是最暴力求解后缀数组的方法,时间复杂度为
【方法二:字符串哈希加速比较】
方法一的效率主要低在比较两个字符串的大小。根据前文对字典序的定义,我们可以用二分 + 字符串哈希找到两个后缀的
这样一来,单次比较的时间复杂度为
【方法三:倍增法】
考虑将
问题转化为,将以某个位置开头,长度为
设
记
我们对于每一个位置
简单理解一下,因为字典序是从左往右比较的,如果左边的半段不一样就比较左边半段,否则比较右边半段。严谨证明的话考虑第一个不同的位置位于哪一半,结合字典序大小关系的定义容易得出上面这个结论是对的。
那么我们需要进行
如果使用 sort
/ stable_sort
,可以做到
但是你会发现上面这份常数太大了。我们可以转变思路,直接求
数组
定义:
特别规定当
代码中有时候
本文代码中,常用
可以说
使用哈希,我们容易
若
和 (其中 )两个后缀存在长度为 的公共前缀,则 , 与 存在长度为 的公共前缀。
时显然。当 时,若它不满足上面这个条件,考虑第一个不同的位置,则会出现 或 的情况,这显然与后缀排序的定义矛盾了。
根据这个引理我们可以推出一条关键性质:
其中
我们考虑
由于
那么
注意到
此时我们可以说
根据
转化成
因为可能存在比它更长的,所以有
我们考虑最暴力的求解
但是运用上面这个性质,我们每次可以从
放一个求解
for (int i = 1, k = 0; i <= n; ++i) {
if (rk[i] == 1) { k = 0; continue; } if (k) --k;
while (a[i + k] == a[sa[rk[i] - 1] + k]) ++k; h[rk[i]] = k;
}
代码中
会求解
,其中 。
首先容易证明它们存在这么多长度的
,严谨证明的话考虑归纳法和 的性质以及等号的传递性。 然后我们可以结合
通过反证法证明它们不存在更长的 。
结合
与某个后缀
的 长度大于等于某个定值 的后缀的 构成一个连续的区间。 换句话说,若排名最小的与
的 长度 的排名为 ,最大的排名为 ,则排名在 内的后缀都满足其与 的 长度大于等于该定值 。
在求解这样的区间时,我们可以建立
到此为止 SA 的所有组成部分就讲解完毕了,附上一份完整的 SA 板子。
//M 为最大数据范围。
template<class T> struct STmin {
T b[22][M];
void build(T *a, int n) { // 对长度为 n 的数组 a 建立 min ST 表。
for (int i = 1; i <= n; ++i) b[0][i] = a[i];
for (int i = 1; (1 << i) <= n; ++i)
for (int j = 1; j + (1 << i) - 1 <= n; ++j)
b[i][j] = min(b[i - 1][j], b[i - 1][j + (1 << i - 1)]);
}
T qry(int l, int r) {
int k = __lg(r - l + 1); return min(b[k][l], b[k][r - (1 << k) + 1]);
}
};
struct SA {
int n, sa[M], rk[M], y[M], cnt[M], h[M]; STmin<int> rmq;
void build(int *a, int m) { // 对长度为 m 的字符串 a 建立 SA,默认字符集与串长同阶。
n = m;
for (int i = 1; i <= n; ++i) ++cnt[a[i]];
for (int i = 1; i < M; ++i) cnt[i] += cnt[i - 1];
for (int i = n; i >= 1; --i) sa[cnt[a[i]]--] = i;
for (int i = 2, t = rk[sa[1]] = 1; i <= n; ++i)
rk[sa[i]] = (a[sa[i]] == a[sa[i - 1]] ? t : ++t);
for (int w = 1, t; w <= n; w <<= 1) {
t = 0; for (int i = n - w + 1; i <= n; ++i) y[++t] = i;
for (int i = 1; i <= n; ++i) if (sa[i] > w) y[++t] = sa[i] - w;
memset(cnt, 0, sizeof cnt); for (int i = 1; i <= n; ++i) ++cnt[rk[i]];
for (int i = 1; i <= n; ++i) cnt[i] += cnt[i - 1];
for (int i = n; i >= 1; --i) sa[cnt[rk[y[i]]]--] = y[i];
swap(rk, y); t = rk[sa[1]] = 1;
for (int i = 2; i <= n; ++i)
rk[sa[i]] = (y[sa[i]] == y[sa[i - 1]] &&
y[sa[i] + w] == y[sa[i - 1] + w] ? t : ++t);
if (t == n) break;
}
for (int i = 1, k = 0; i <= n; ++i) {
if (rk[i] == 1) { k = 0; continue; } if (k) --k;
while (a[i + k] == a[sa[rk[i] - 1] + k]) ++k; h[rk[i]] = k;
}
rmq.build(h, n);
}
int lcp(int x, int y) {
if (x == y) return n - sa[x] + 1;
if (x > y) swap(x, y); ++x; return rmq.qry(x, y);
}
pair<int, int> range(int x, int y) {
int l = 1, r = x, m, f, g;
while (l <= r) {
m = l + r >> 1;
if (lcp(m, x) >= y) f = m, r = m - 1; else l = m + 1;
}
l = x; r = n;
while (l <= r) {
m = l + r >> 1;
if (lcp(m, x) >= y) g = m, l = m + 1; else r = m - 1;
}
return make_pair(f, g);
}
} S;
简单应用
本质不同子串个数
给出长度为
的字符串 ,求 有多少个本质不同的子串。
。
首先,任意一个子串一定是一个后缀的前缀。比如说
那么,对于任意一种本质不同的子串,考虑所有存在它为前缀出现的后缀,在这样的排名最小的后缀中统计它。
那么就是要对于排名为
那么累加每一个后缀的贡献即可。
时间复杂度为
处理多串问题
给出字符串
和 个字符串 ,求 在 中出现了多少次。
。
对于
但是我们无法直接求解来自于两个串的后缀
对于某个串以
我们需要保证两点:
-
任意两个后缀在大串中的对应后缀的大小关系和原串一致。
-
任意两个后缀在大串中的对应后缀的
长度和原串相同。
这里直接给出拼接的方法:
我们用数去表示字母,这样适用于更大的字符集。对于第
首先来看第一点。若两个后缀
然后看第二点,若两个后缀不存在前缀包含的关系,同样考虑失配位置在大串中仍然存在,且在其之前的字符都能作为
那么对于原问题,我们可以求,有多少个
时间复杂度为
习题
SP10419 POLISH - Polish Language
给出字符串
,求有多少个序列 满足:
数量对
取模。其中字符串的比较均基于字典序大小。 多组数据,
。
看到后缀之间的字典序比较,先想到后缀数组。处理完之后,考虑一个一个解决限制条件。
-
,不用转化。 -
,等价于 。 -
,等价于 。
典型二维偏序,考虑 dp。设
倒序枚举维护
设数据组数为
P5353 树上后缀排序
给出一棵
个节点,以 为根的树,点 上有字符 。定义点 的字符串 为从点 走到点 路径上所有点上的字符拼接而成的字符串。 形式化的,若点
到点 的路径为 ,则 。 你要对
这些点按照 的字典序进行排序,若字典序相同,则父亲排名小的点排名小。若仍相同,编号小的点排名小。
。
看到对字符串排序想到后缀排序。我们可以类比后缀排序,利用倍增的思想,每次将两条长度为
for (int l = 0, id; (1 << l) <= n; ++l) {
for (int i = 1; i <= n; ++i)
p[i] = {{rk[i], fa[l][i] ? rk[fa[l][i]] : 0}, i}; // 先比前半段,前半段相同再比后半段。
memset(cnt, 0, sizeof cnt);
for (int i = 1; i <= n; ++i) ++cnt[p[i].fi.se];
for (int i = 1; i <= n; ++i) cnt[i] += cnt[i - 1];
for (int i = n; i >= 1; --i) tmp[cnt[p[i].fi.se]--] = p[i];
for (int i = 1; i <= n; ++i) p[i] = tmp[i];
memset(cnt, 0, sizeof cnt);
for (int i = 1; i <= n; ++i) ++cnt[p[i].fi.fi];
for (int i = 1; i <= n; ++i) cnt[i] += cnt[i - 1];
for (int i = n; i >= 1; --i) tmp[cnt[p[i].fi.fi]--] = p[i];
for (int i = 1; i <= n; ++i) p[i] = tmp[i]; id = 0;
for (int i = 1; i <= n; ++i)
{ if (i == 1 || p[i].fi != p[i - 1].fi) ++id; rk[p[i].se] = id; }
if (id == n) { op = 1; break; }
}
关键是如何去重。这个代码求出来的
memset(cnt, 0, sizeof cnt); for (int i = 1; i <= n; ++i) ++cnt[rk[i]];
for (int i = 1; i <= n; ++i) cnt[i] += cnt[i - 1];
for (int i = 1; i <= n; ++i) rk[i] = cnt[rk[i] - 1] + 1;
然后考虑两个字典序相同的字符串,它们的深度一定相同。因此用 vector
存放深度为
bool cmp1(int u, int v) { return rk[u] < rk[v]; }
bool cmp2(int u, int v) {
return rk[fa[0][u]] != rk[fa[0][v]] ? rk[fa[0][u]] < rk[fa[0][v]] : u < v;
}
for (int i = 0; i < n; ++i) {
sort(h[i].begin(), h[i].end(), cmp1);
for (int j = 0, k, l = h[i].size(), id; j < l; j = k) {
for (k = j; k < l && rk[h[i][k]] == rk[h[i][j]]; ++k);
sort(h[i].begin() + j, h[i].begin() + k, cmp2); id = rk[h[i][j]] - 1;
for (int d = j; d < k; ++d) rk[h[i][d]] = ++id;
}
}
去完重之后,如果用后缀排序中的名称来说,别忘记你输出的是
现在来算时间复杂度,倍增处理
综上,本做法时间、空间复杂度均为
AT_s8pc_2_e 部分文字列
给出一个字符串
,求 的所有本质不同子串的长度之和。
。
看到本质不同子串,先做后缀排序把
时间复杂度为
CF149E Martian Strings
给出字符串
,以及 个询问串 ,每次询问是否能找到两个不交的区间 使得 。
, , 。
考虑将所有串拼成一个大串
对于每一组询问,考虑枚举
我们考虑选择最左边的
时间、空间复杂度均为
CF316G3 Good Substrings
附 CF316G1 Good Substrings,CF316G2 Good Substrings。
给出字符串
,以及 个限制,每个限制形如 ,一个字符串满足该条限制,当且仅当它在字符串 中的出现次数在 之间。 求
有多少个本质不同的子串满足所有限制。
, 。
记
看到「本质不同子串」,想到后缀数组。先将所有字符串用奇怪字符拼起来(记大串为
对于一个字符串
因为若有一个长串出现若干次,我的短串也被这个长串包含,至少出现了这么多次。
考虑二分出最短的满足所有限制上界的字符串长度
维护一个前缀和数组
还有一个地方需要注意,我们要求的是原串
时间复杂度为
UVA1502 GRE Words
给出
个字符串 ,第 个字符串有权值 。选出一个子序列 ,满足 且 是 的子串。求 的最大值。可以为空,此时权值为 。
组数据, 。对于单组数据,满足 , 。
记
考虑 dp。设
将所有串拼成一个大串
这样的后缀排名形如一个区间
那么转移就是区间最大值。可能会重复贡献一些
转移完后在线段树上对
时空复杂度均为
SP8093 JZPGYZ - Sevenk Love Oimaster
- 给出
个模板串 和 个查询串 ,每次询问一个查询串是多少个模板串的子串。 , , , 。
先将所有字符串用奇怪字符拼成一个大串
对于每一个询问串,以它为前缀的后缀的排名一定是一个区间,考虑二分出这个区间
我们记排名为
主席树维护每一个位置的前驱,数有多少个前驱在区间外即可。但是询问串之间也可能存在包含关系,所以要数的颜色必须是
因此主席树的一个节点的定义为:当前版本中,有多少个位置满足这个位置的颜色在
时间、空间复杂度均为
CF232D Fence
给出序列
,有 次询问,每次询问给出 ,求有多少个区间 满足 , 且 。
。
tags:
原题就是让我们求出有多少个满足条件的左端点。
我们记原数组的差分数组
- 证明:
根据已知条件可以推出:
两式相减即可得到
,即 。
我们若倍长
为什么要做这一步转化呢?我们发现,对于
这就是个二维数点,在线主席树或离线扫描线 + 树状数组维护一下就行了。
- 注意
使用上述统计方法的前提是存在差分数组。当
时,区间内不存在差分数组,不能这样统计。 不过容易得知此时答案即为
,特判一下即可。
代码里用的是主席树,时间、空间复杂度均为
P4143 采集矿石
给出字符串
,以及数组 。 定义一个子串的排名为:字典序比它大的本质不同的子串个数
。 定义一个子串
的权值为 。 求有多少个子串的排名等于权值。
。
首先对
所以可以二分出满足条件的最小 / 大右端点。
考虑如何求出一个子串
前半部分运用经典结论即为
可以二分出以这个子串为前缀的后缀排名区间
-
充分性:
若一个子串
在排名为 的后缀中作为前缀出现,那么这个后缀 与 的 长度一定小于 。即两个后缀可以在第 个位置之前可以找到不相同的位置。而由于 这个后缀排名更小,在这个位置一定 这个后缀小于 。考虑
是否跨过这个位置,若不是,则在前 位两串相同,第 位 为空,字典序极小。若跨过,则
在这个位置小于 。 -
必要性:
考虑这两个子串第一次不同是在某个位置,这个位置一定在两个后缀中。
正确性证好了。这个东西也是考虑每个后缀带来的本质不同子串。即可以这么求:
于是做完了。时间复杂度为
ABC280Ex Substring Sort
给出
个字符串 。记 表示 这个子串。将所有存在的 非降排序。 次询问,求出排在第 位的 。如有多解输出任意一个。
, , 。
。
先将所有字符串拼成大串
称一个串在排名为
对于
考虑求出答案是第几大的本质不同子串。容易发现排在第
记二分的子串为
对于第一部分,这些后缀的所有前缀都小于
对于第二部分,答案为
第一部分答案即为
那么第一种情况的贡献就是
可以用单调栈,对于每个
这样一来我们就找到有多少个子串小于给定的串
那么这题就做完了,时间复杂度为
CF1037H Security
给出一个字符串
,有 次询问,第 次询问给出 ,求一个字典序最小的字符串 ,使得它是 的子串,且 。
, 。
记
证明很简单,假设有一个串
先将
对于每一组询问,考虑枚举
考虑继续二分出这个连续区间
我们要求
可以从小到大枚举
对于多种
默认
P4770 [NOI2018] 你的名字
给出字符串
以及 个询问,第 个询问给出一个串 以及一个区间 。 记
为字符串 第 位到第 位字符顺次拼接而成的子串。形式化地, 。 对于每个询问,求
有多少种本质不同的子串没有在 中出现。
。
。
神仙字符串题。
首先把所有字符串用特殊字符接起来,得到一个大串
对于每一组询问,考虑容斥,即用
前半部分是平凡的,即按排名考虑每一个后缀带来的本质不同子串个数,根据经典结论就是这个后缀的前缀数减去它的
至于后半部分,同样这样考虑每个后缀带来的本质不同子串中有多少个在
二分出排名区间,主席树二维数点检查即可。得到这个值后,
但是这样回答单组询问的时间复杂度为
思考一下二分的目的,我们想要对于
我们发现一个关键性质,那就是
我们可以类似于
由于最多递减
综上,我们得到了一个时间、空间复杂度均为
P4022 [CTSC2012] 熟悉的文章
给出
个文本串 和 个询问串 。 称一个字符串
是“ 熟悉的”,当且仅当 ,且 是文本串的子串,此时记 。否则 。 对于每个询问串
,求出最大的整数 ,使得将其划分为若干个子串后,所有“ 熟悉的”子串长度之和不小于 。 形式化地,记一种划分
的方式为 ,满足 ,且 。记所有划分方案构成的集合为 。你要找到最大的整数 ,满足 。 记
,满足 。
。
默认
对于每一个询问,容易发现答案有单调性,因为若
对于一个已知的
设
就是去考虑这一段能否成为“
至于后面那部分,先将
将所有串用分隔符拼在一起形成大串
进一步发现,若
考虑如何判断一个长度
满足后面那个条件的后缀排名形如一段区间
但是这样对于每个后缀做一遍时间复杂度为
注意到一个关键性质,
那么这样用个指针扫一下即可,指针最多递增
这样我们就求出了
时空复杂度均为
各种卡常、指令集配合 C++98
艹过去了。
CF1483F Exam
- 给出
个字符串 ,求有多少对 ,满足:
。 是 的真子串。 - 不存在
( 两两不同)使得 是 的真子串,且 是 的真子串。 。若 ,则 。 。
先将所有串拼成一个大串
考虑对于
构造一个二元组不可重集合
。 有意义。 。- 不存在
,使得 。
称
再构造一个二元组不可重集合
。 。
那么原题目中的二元组
证明:
充分性:
考虑反证,假设当
时存在 使得 是 的子串且 是 的真子串。 设
, 。那么 。根据已知条件可以得到 。 若
,则与 的第三个条件不符。 否则,此时
。根据 的定义可知 ,即 。但是 。根据 可以得到 。与 的第四个条件不符。 因此假设不成立。当
时一定不存在 使得 是 的子串且 是 的真子串。 必要性:
考虑
但是 。此时 一定满足某个二元组在 中的前两个条件。 若
,则有一个更长的字符串 为 的前缀。此时 为 的真子串。 否则,一定存在
使得 。说明存在一个字符串 。此时 把 包含在中间,即 是 的真子串(能保证是真子串是因为 )。 所以不满足
一定不会满足原来的条件,这是一个必要条件。
光有这个结论还不够,总不可能求出集合然后枚举判断。
进一步推理可以得到,它其实等价于
为了区分中括号和艾弗森括号,使用
为什么呢?不难发现
那么我们只需要对于一个
-
前者的求法:
首先要得到
。可以考虑对于每个字符串 ,它会对哪些排名的后缀的产生贡献。这个后缀要包含 ,等价于两者 。可以维护 数组的 ST 表然后二分得到排名区间,让这个区间内的 值对 取 。线段树维护即可。然后就可以从左往右扫,维护前缀的
最大值 。在线段树上单点查询当前后缀排名那个位置的值得到 。若 ,则将 加入 。然后使用桶维护
中 中每种字符串各作为多少个后缀的最长前缀。 -
后者的求法:
考虑
作为某个后缀的前缀出现,同样可以求出包含它的后缀排名区间。然后变成求区间内有多少个排名使得这个排名的后缀来自于编号为 的字符串。对于每个
用一个vector
从小到大存放其后缀出现的位置,二分得到左右端点 ,答案即为 。
这样仍需要枚举
由于
为了不算重算漏,考虑对于每个在
最后对
时空复杂度均为
CF1608G Alphabetic Tree
给出一棵
个点的树,边 上有字母 。定义 为从点 走到点 途径边上的字母顺次拼接得到的字符串。形式化的,若点 到 点 路径上的边依次为 ,则 。 你有
个字符串 和 个询问,每个询问形如 ,你要回答 在 中出现了几次。在一个串中重复出现算多次。
。
考虑将答案表示成差分的形式并离线计算,即
先套路将所有串用奇怪字符拼接起来(拼成的大串记为
记
我们试图把
因此,我们想要求出这些后缀排名的区间。我们发现
考虑二分出字典序大于等于
考虑使用哈希。若按照常规方法直接使用树剖、线段树或树上倍增维护路径的哈希值再二分
我们发现二分
具体地,先计算
每次询问
然后遍历容器,一段段与排名为
这部分细节繁多,比如匹配的方向、需要匹配的长度、还未匹配的长度、以及一方匹配完之后如何比较大小等。为了方便,这个过程需要求出两个值,
求出来后,若
设当前扫描到的右端点为
时间复杂度为
考虑如何在线地解决这个问题。
这个时候我们把离线树状数组换成主席树即可,将所有后缀
时间复杂度为
UVA10829 L-Gap Substrings
以此题开始连续三题用到的都是同一个套路,做法阐述可能有重复累赘之处,但是都是从原来的题解里复制粘贴的,对于三题的做法都保留了对于相同套路的阐述部分。
给出字符串
和常数 ,求出有多少四元组 ,满足 且 。
组数据, , 。
先后缀排序。
考虑一对合法的
我们再考虑对于一个
我们发现合法的二元组与合法的四元组一一对应。
那么只需要统计有多少满足条件的
考虑转化成
套路地分治,设当且分治区间为
从
对于
对于
考虑一个智慧的容斥,对于
由于对于
此时,两部分
注意这里拆绝对值和解不等式的细节,尤其是不能忽略
然后往两半递归求解即可。
这题就做完了,时间复杂度为
P9623 [ICPC2020 Nanjing R] Baby's First Suffix Array Problem
给出长度为
的字符串 , 组询问对 这个子串进行后缀排序后,(这个子串的)后缀 的排名。排名定义为比它小的后缀的个数 。 多组数据,记
, , 。
。
这个
先对原串进行后缀排序。
考虑从排名的定义入手,求出子串中有多少个后缀比询问的后缀小。对于这些子串中的后缀,考虑找到它们在原串中的后缀,尝试寻找充要条件。
设有(子串的)后缀
-
此时
,当且仅当 且 ,或 。-
充分性
当
且 时,两个后缀第一个不同的位置一定均在 和 中出现,此时比较两个串也是比较这两位,因为 ,故 。当
时,若两个后缀第一个不同的位置均在 中出现则与上一种情况合理,否则 是 的前缀,故 。 -
必要性
考虑
时,若 ,则一定有 ,否则 为 前缀,此时 。若 ,则已经满足条件。 -
做法
分
和 讨论。若
,则需要统计有多少个后缀 满足 , 且 。降第三个限制转化为 数组的限制,其等价于 。容易发现此时满足条件的 的 在一个前缀 中,其中 。二分 + RMQ 求出这个 ,问题转化成统计有多少个点对满足 且 ,主席树维护即可。若
,则需要统计有多少个后缀 满足 且 ,同样主席树维护。
-
-
此时
,当且仅当 且 。-
充分性
容易发现此时
为 前缀,故 。 -
必要性
考虑证明不满足上述条件则
。若
,如果两个串第一个不同的位置均在 中出现,因为 ,所以 。否则, 为 前缀,此时 。若
且 ,则两个串第一个不同的位置一定均在 中出现,因为 ,所以 。 -
做法(本题解最核心部分)
以排名为下标做一遍序列分治,将询问挂在
上,每层分治考虑右半边对左半边的贡献(很像 cdq 分治)并左右递归下去统计,则对于任意一个合法的后缀,根据分治树的形态,一定存在且仅存在一层分治,使得询问在左半边,后缀在右半边,此时它被统计到。并且,在每层分治中我们统计合法的贡献,可以做到不重不漏。设分治区间为
,中点 。对于左半边的一个询问
,我们要统计右半边有多少个 ,满足:采用序列分治的一般套路,从
扫描询问。设当前扫到的排名为 。维护变量 。对于右半区间维护前缀 最小值,即 。则对于当前扫到的排名上的询问,条件中的 可以转化为 。容易发现
具有单调(不升)性。可以找到一个分界点 ,使得当 时, ;当 时, 。对于分界点左边的情况,就是统计有多少
满足:整理一下就是:
容易主席树维护。
对于分界点右边的情况,就是统计有多少
满足:你发现这是个三维数点,好像行不通啊!
然后就是一个很妙的转化了。考虑正难则反。你发现对于分界点右边的情况,
,因为在分界点右边 。所以可以先统计满足以下条件的 的个数:算上分界点左边的统计,相当于要统计右半边满足
的 个数。可以vector
+ 二分统计。考虑哪些不合法的被统计了,显然它满足:于是就要减去这样的
的个数。实际上这还是个三维数点,不过你发现, 。即分界点左边不存在满足前两个条件的 。为什么呢?首先
的必要条件是 。你考虑分界点左边 ,若 即 ,则一定有 。反之,若 ,则一定有 即 。因此两个条件不能被同时满足。所以我们直接大胆忽略
这个条件,统计全局(当前分治区间) 且 的 的个数。同样是二维数点,主席树维护即可。 -
-
至此两类统计都解决了。接下来算复杂度。因为有主席树和 ST 表,所以空间复杂度显然为
至于时间复杂度(只说每个部分的瓶颈),后缀排序是
对于分治部分,每个询问会在 vector
二分,单次是
综上,本做法时间复杂度为
CF1098F Ж-function
给出长度为
的字符串 。定义 。 次询问,每次给出 ,查询 。
, 。
先后缀排序。
将子串的
然后将询问挂在
我们先以
对
记
考虑挂在
此时,存在
化简第二层
由于
然后将
对于
对于
接下来是重点,也是这个套路最巧妙的一步。
如果按照之前的方法找偏序关系,发现是关于
我们先求
由于右半边
那么这种情况就讨论完了。剩下的一种情况是类似的,尤其是对于
至于
那么这题就做完了。
记
CF587F Duff is Mad
- 给出字符串
, 次询问 在 中作为子串出现的次数。 。 。
首先将全部询问离线,维护一个前缀中的串在某个串中的出现次数然后差分解决。将所有串拼成大串
我们可以将在一个字符串中出现转化为在一个后缀作为前缀出现。当
串长一定时可以阈值分治。设阈值为
对于
考虑使用分块解决。对于小串,使用
对于大串,每个都用分块维护一个值域前缀和表示集合中不超过某个值的元素个数,那么对于这个大串排名集合中的一个元素,它产生的影响是一段后缀的值域前缀和加
注意此时空间复杂度为
此时,取
其它复杂度的 SA 做法参见题解区。
P8203 [传智杯 #4 决赛] DDOSvoid 的馈赠
给出
个模板串 和 个查询串 。有 次询问,每次给出 ,求有多少个模板串同时是 的子串。
。
。
考虑将所有串用分隔符拼一起后缀排序,然后对于每个排名为
其实来自分隔符和查询串的后缀是没用的,因此可以在
将
考虑转化后的问题,记
遍历
首先需要满足
若存在,则应满足
那么上面讨论的这些情况的答案全部都是二维数点,容易解决。
问题是暴力遍历
考虑将
那么变成
还有一个问题,怎么快速求出
那么我们得到了一个时空都是
事实上
CF917E Upside Down
给出
个点的树,第 条边上有字母 。有 个字符串 以及 组询问。每次询问给出 。 记
为 简单有向路径边上的字母按顺序拼接得到的字符串,形式化地,若 简单有向路径上一共有 条边,记 为 有向路径上的第 条边,则 。 求
在 中出现了多少次。形式化地,求有多少个正整数 使得 。 记
, 。
。
约定:
-
本文中所有下标均从
开始。钦定 为根。用打印机字体(\texttt
)表示具体的字符 / 字符串。 -
默认
, 。 -
记一个点
的父亲为 ,深度(到根的边数)为 。 -
表示 到 的简单有向路径, 这条边上的字符为 。特别地, 。 -
表示 两点的最近公共祖先。 表示两个字符串 的最长公共前缀。 -
记一个串
的反串为 。形式化地, 且 。 -
表示 向上走 条边到达的点,即 的树上 级祖先。 -
若字符参与运算,则其值等于其
值。
考虑弱化版 CF1045J 的做法,
在这部分中考虑使用哈希实现字符串匹配。我们的哈希方式为多项式哈希,即对于字符串
其中
在弱化版中,我们运用
在字符串总长度为
的长为 的字符串序列 中,本质不同的字符串长度种数为 级别。
考虑
种出现了 种本质不同的长度,从小到大依次是 ,记 表示第 种长度的出现次数,形式化地, 。 那么有:
。 可以发现
。后面那个很显然,因为这种长度出现时一定存在一个字符串满足其长度为 。 至于前面那个使用归纳法证明:
当
时 显然成立。 假设对于
时成立,则对于 时,由于 中的每一个数都是一种本质不同的长度,且从小到大排列,所以 。由于都是整数,所以 。 由于这里涉及到的量都是正的,所以
,因此 ,因此有 。可以得到 。注意这里不是在解不等式,由 推导出一个成立的条件。 证毕。
那么我们可以对于这
由于这个做法比较垃圾,我们不能在求解每种长度时重新遍历树计算哈希值,否则会超时。可以考虑牺牲空间,在第一次遍历树时就对于每个点存下这些哈希值。这样可以省去
具体地:
可以预处理
在求解每种长度时再考虑对于每种询问串的哈希值
至于询问,对于
此时,一共有
值得注意的是,为了将同种哈希值的询问一起做,考虑使用排序将它们排在一个连续的区间内时,需要使用基数排序确保排序复杂度线性,才能保证
考虑分别处理每种串
假设跨过直链的匹配发生在
同时,
考虑找到最长的长度
考虑一个基础问题:
给出字符串
,找到 的最长前缀使得它是 的后缀。求出这个最长长度。
解决方法是:找到
接下来证明正确性。
首先,这个
一定是同时是 的前缀和 的后缀。因为它是 的前缀又是它的 ,说明 存在这个 作为后缀。自然 也存在这个 作为后缀。记这里求出来的长度为 。 考虑是否存在更长的答案。假设存在更长的答案长度为
,其一定不超过 ,不然 就不是使得 长度更大的后缀了。此时, 与 存在长度为 的 。这时候 开头的 个字符形成的字符串与结尾的 个字符形成的字符串相等。此时 是 的一个更长的、长度不超过 的 ,矛盾。 因此不存在更长的答案,
即为所求。 证毕。
将原问题转化成上述形式,那么
因此,
先对
最长的长度不超过
因此,对于
接着考虑如何找到使得
接下来给出证明:
设它们的排名分别为
。则一定有 。因为根据定义,排名为 的后缀字典序大小已经超过了 ,但是排名在 内的后缀字典序大小都不超过 。 考虑反证,假设排名为
的后缀会得到更大的 长度。 记这个更大的
长度为 。分两种情况讨论:
若
,则排名为 的后缀的前 位均与 的前 位相同。根据 的定义可知其第 位也与 的这一位相同。根据定义,排名为 的后缀的第 位小于 的这一位,或者说这一位不存在(空字符)。此时,排名为 的两个后缀前 位相同都等于 的前 位。且后者的第 位大于前者的这一位。说明后者比前者字典序大,这与 矛盾。 若
,与上一种情况类似推导得到字典序大小上的矛盾即可证明。 证毕。
于是考虑求得排名
考虑如何求一条链上的字符串和序列上的字符串的最长公共前缀长度。对原树进行轻重链剖分,将边权转化为深度较深的端点的点权,则这条链会被表示成
一条一条重链匹配,若能全部匹配上,就算上这些长度,否则二分第一个不同的位置。只有第一条不匹配的重链需要二分,因此时间复杂度为
这部分细节比较多,尤其是一方匹配完的边界情况,具体看代码中的 qlcp
部分。
此时,这个过程已经求出了
那么
求出来之后,我们只需要考虑
记这
则
存在长度为 的 。 存在长度为 的 。 。
证明:
充分性:
因为
,所以跨过了 。因为 存在长度为 的 ,根据 的定义可以得到 ;因为 存在长度为 的 ,类似地, ,根据反串的定义得到 。两者拼接恰好是 。 必要性:
若
开头处可以形成合法的匹配,首先一定有 ,因为要跨过 。其次 ,根据 的定义, ,因此 ,即 存在长度为 的 ;类似地, ,因此 存在长度为 的 。 证毕。
所以,我们要统计有多少
转化成失配树上的限制,就是要求有多少
考虑离线 + 扫描线。对于所有
在 的当前搜到的点 的根链上。 在 中点 的根链上。 。
则只要在
每次新扫到一个点
这部分就做完了,时间复杂度为
综上,这个做法时空复杂度均为
参考资料
-
作者:@Alex_Wei
-
本文涉及到题目的题解区。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧