字符串基础
推荐在 cnblogs 阅读。
前言
几乎所有字符串算法都存在一个共性:基于所求信息的特殊性质与已经求出的信息,使用增量法与势能分析求得所有信息。这体现了动态规划思想。
Manacher 很好地证明了这一点:它维护所求得的最右回文子串的回文中心
SA,KMP,Z 与 SAM 等常见字符串算法无不遵循这一规律。读者在阅读时注意体会这种思想,笔者认为其对于提升解决问题的能力有很大帮助。
定义与记号
约定
- 文章使用打字机字体
texttt
描述一个字符串的具体内容,例如 。 - 无歧义时,
表示当前描述的字符串的长度。在字符串两侧加上 表示长度。 - 当下标
不在 范围内时,对应位置的字符 视为空字符。
基本定义
- 字符集:字符集 可以是任何具有全序关系的集合
,即 中任意两个不同元素 ( )可比较大小。除非特殊规定,一般按字母表顺序或数码大小比较元素。 - 字符:字符集
中的元素称为 字符。 或 表示 的第 个字符。 - 空串:不含任何字符的字符串称为 空串,记作
, 。空串之于字符串类似空集之于集合。 - 子串:由
在开头或末尾删去若干字符得到的字符串称为 的 子串。 本身和空串均为 的子串。 或 表示 位置 上的字符连接而成的子串,当 时为空串。 - 反串:翻转
得到 的 反串,记作 。 - 回文串:
的串称为 回文串。特别地,空串是回文串。 - 拼接:
表示将 拼接在 后。
前后缀相关
- 前缀:在
末尾删去若干字符得到的字符串称为 的 前缀,形如 ( ),记作 。 本身和空串均为 的前缀。 - 后缀:在
开头删去若干字符得到的字符串称为 的 后缀,形如 ( ),记作 。 本身和空串均为 的后缀。 - 真前 / 后缀:真前缀 表示非原串前缀,真后缀 同理。
- 最长公共前缀:
表示 和 的 最长公共前缀(Longest Common Prefix),即最长的 使得 同时为 和 的前缀。最长公共后缀(Longest Common Suffix)同理。 - 字典序:定义空字符小于任何字符。称
的 字典序 小于 当且仅当去掉 后, 的第一个字符小于 的第一个字符。等价于以第 个字符作为第 关键字比较。
匹配相关
-
出现位置:若
,则称 在 中以位置 出现。 在 中的 出现位置 等于 的最后一个字符在 中的对应位置。例如,若 , ,则 在 中的所有出现位置为 。 -
匹配:称字
匹配 当且仅当 在 中出现。 -
模式串(单词):用于匹配的字符串称为 模式串,相当于题目给定的 单词。
-
字典:题目给定的所有模式串的集合称为 字典。
-
文本串:被匹配的字符串称为 文本串。
-
分清模式串和文本串的定义:用
匹配 即求 在 中的所有出现位置, 是 用于匹配的串,称为模式串(模式串是我们要寻找的模式,是子串); 是 被匹配的串,称为文本串(文本串相当于给出的文本,是主串)。
1. Manacher 算法
Manacher 在所有字符串算法中理解起来相对容易,学习它有助于理解 Z 算法。
Manacher 在 NOI 大纲里是 8 级算法。
1.1 相关定义
根据回文串的定义,我们发现奇回文串和偶回文串本质不同。当
此外,定义回文串
容易发现对于
- 当它是下标
时,存在阈值 满足当 时 回文, 称为以位置 为回文中心的 最长回文半径 。它可以理解为以 为回文中心的回文子串数。 - 同理,当它是
与 之间的空隙时,存在阈值 满足当 时, 回文。
1.2 统一奇偶回文子串
为避免分类讨论,我们尝试将所有偶回文子串转化为奇回文子串。
容易发现,若将所有空隙视作一种独立的 分隔字符,则偶回文子串可视为以对应分隔符为回文中心的奇回文子串。例如
考虑原串偶回文子串在新串上的形态。偶回文子串
考虑原串奇回文子串在新串上的形态。奇回文子串
这样,我们统一了奇回文串和偶回文串从新串转回原串的过程,有效减少了根据最长回文半径
1.3 算法介绍
Manacher 算法可以求出以每个位置
首先将
朴素想法是对每个回文中心
只需加上一些优化即可将复杂度变为
回顾朴素暴力,在从
具体地,在
如上图,因为黄色部分回文,所以对于以
因此,考虑维护已经计算过的所有回文中心对应的最长回文子串的右端点的最大值
对于当前位置
否则
- 若
,则 就等于 。否则根据对称性 可以更大,与其最大性矛盾。 - 否则
被初始化为 ,使得每次扩展都会将 向右移动 。
综上,时间复杂度线性。
模板题 P3805 代码如下:
#include <bits/stdc++.h>
using namespace std;
const int N = 2.2e7 + 5;
int n, m, ans, R[N];
char s[N], t[N];
int main() {
scanf("%s", s + 1), n = strlen(s + 1);
t[0] = '!', t[m = 1] = '@';
for(int i = 1; i <= n; i++) t[++m] = s[i], t[++m] = '@'; // 间隔插入字符.
t[++m] = '#';
for(int i = 1, c = 0, r = 0; i < m; i++) { // r 是最右回文边界, c 是对应回文中心.
R[i] = r < i ? 1 : min(R[c * 2 - i], r - i + 1); // 若 i <= r, 则根据对称性继承信息.
while(t[i - R[i]] == t[i + R[i]]) R[i]++;
ans = max(ans, R[i] - 1);
if(i + R[i] - 1 > r) c = i, r = i + R[i] - 1; // 更新 r 和 c.
}
cout << ans << endl;
return 0;
}
1.4 结论与应用
Manacher 本身证明了一个关于回文子串的结论:一个字符串的本质不同回文子串个数不大于
当然,我们也可以不借助 Manacher 直接证明这一结论。考虑在每个回文子串第一次出现处计入贡献。若存在两个本质不同回文子串
利用 Manacher,我们可以求出以每个字符开头或结尾的最长回文子串:考虑位置
1.5 例题
UVA11475 Extend to Palindrome
找到最小的使得
P3501 [POI2010] ANT-Antisymmetry
借鉴 Manacher 的思路,我们对每个位置求出最长 Anti-symmetry 半径。同样的,记录最右边的回文区间,快速继承和扩展做到均摊线性。代码。
P4555 [国家集训队] 最长双回文串
对每个位置求出以该字符结尾和开头的最长回文子串
时间复杂度线性。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 5;
int n, m, ans, R[N], lft[N], rt[N];
char s[N], t[N];
int main() {
scanf("%s", s + 1), n = strlen(s + 1);
t[0] = '!', t[m = 1] = '@';
for(int i = 1; i <= n; i++) t[++m] = s[i], t[++m] = '@';
t[++m] = '#';
for(int i = 1, c = 0, r = 0; i < m; i++) {
R[i] = i > r ? 1 : min(R[2 * c - i], r - i + 1);
while(t[i - R[i]] == t[i + R[i]]) R[i]++;
if(i + R[i] - 1 > r) {
for(int j = r + 1; j < i + R[i]; j++) if(j & 1 ^ 1) lft[j >> 1] = j - i + 1; // 新串的偶数位置对应一个原串字符.
c = i, r = i + R[i] - 1;
}
}
for(int i = m - 1, c = m, r = m; i; i--) { // 倒过来做一遍 Manacher.
R[i] = i < r ? 1 : min(R[2 * c - i], i - r + 1);
while(t[i - R[i]] == t[i + R[i]]) R[i]++;
if(i - R[i] + 1 < r) {
for(int j = i - R[i] + 1; j < r; j++) if(j & 1 ^ 1) rt[j >> 1] = i - j + 1;
c = i, r = i - R[i] + 1;
}
}
for(int i = 1; i < n; i++) ans = max(ans, lft[i] + rt[i + 1]);
cout << ans << endl;
return 0;
}
P1659 [国家集训队] 拉拉队排练
因为题目要求奇回文串,所以只考虑以下标
时间复杂度是快速幂的
P5446 [THUPC2018] 绿绿和串串
如果
Manacher 求出每个位置的最长回文半径后倒过来 dp 一遍即可。
时间复杂度
2. Z 算法 / 扩展 KMP
扩展 KMP 在 NOI 大纲里是 9 级算法。它和 KMP 没有关系。
2.1 算法简介
定义字符串
每次暴力匹配,时间复杂度
Z 算法利用已经求得的信息的性质,通过增量法求出 Z 函数。
称
类似 Manacher,实时维护最靠右侧的匹配段
- 若
,直接暴力匹配。 - 若
,因为 ,所以 。因此 的 前缀和 的 前缀相等。故首先令 ,然后暴力匹配。
读者可以发现 Z 算法和 Manacher 的核心思想几乎一模一样。
时间复杂度:当
2.2 应用
Z 函数可以用于做特定类型的字符串匹配:求字符串
- 解法 1:令
,其中 是任意不属于 字符集的分隔符。对 求 Z 函数。 - 解法 2:先求出
的 Z 函数。然后类似求 Z 函数的方法,维护最右匹配段 表示 ,若 则暴力匹配,否则令 。
两种解法本质相同,因为 Z 算法本身相当于用
2.3 例题
P5410 【模板】扩展 KMP(Z 函数)
#include <bits/stdc++.h>
using namespace std;
const int N = 2e7 + 5;
char s[N], t[N];
int n, m, z[N], p[N];
int main() {
scanf("%s%s", t + 1, s + 1);
m = strlen(t + 1), n = strlen(s + 1);
z[1] = n;
for(int i = 2, l = 0, r = 0; i <= n; i++) {
z[i] = i > r ? 0 : min(z[i - l + 1], r - i + 1);
while(s[1 + z[i]] == s[i + z[i]]) z[i]++; // 因为 i 不等于 1, 所以 i + z[i] 超出下标时空字符和 s[1 + z[i]] 必然不等, 判断句不成立.
if(i + z[i] - 1 > r) l = i, r = i + z[i] - 1;
}
for(int i = 1, l = 0, r = 0; i <= m; i++) {
p[i] = i > r ? 0 : min(z[i - l + 1], r - i + 1);
while(p[i] < n && s[1 + p[i]] == t[i + p[i]]) p[i]++; // 应判断 p[i] 小于模式串长度而非匹配串长度.
if(i + p[i] - 1 > r) l = i, r = i + p[i] - 1;
}
long long ans = 0;
for(int i = 1; i <= n; i++) ans ^= 1ll * i * (z[i] + 1);
cout << ans << endl;
ans = 0;
for(int i = 1; i <= m; i++) ans ^= 1ll * i * (p[i] + 1);
cout << ans << endl;
return 0;
}
CF432D Prefixes and Suffixes
KMP 找到完美子串,Z 算法 + 差分求出现次数。
时间复杂度线性。代码。
CF526D Om Nom and Necklace
重新表述题意:若
枚举所有
此时考虑可能的
时间复杂度线性。代码。
3. 后缀数组
后缀数组(Suffix Array, SA)的思想与实现非常简单,基础的倍增思想加上排序,但其扩展得到的
后缀数组在 NOI 大纲里是 8 级算法。
3.1 相关定义
- 定义
表示 在所有后缀中的字典序排名。由于任意后缀长度不同,故不存在两个后缀排名相同。例如 ,有 ,所以 。 - 定义
表示排名为 的后缀的开始位置。它与 互逆: ,这个 表示排名; ,这个 表示位置。这就是 后缀数组。
也可以说,
充分熟悉
- 简记
和 的最长公共前缀为 。 数组的定义将在 3.3 小节给出,它是 SA 算法的核心。- 下文区分下标和位置两个概念,前者指数组的某个位置,而后者指某个后缀的开始位置。
3.2 后缀排序
后缀排序算法通过一系列排序操作得到一个字符串的后缀数组。
我们的目标是将字符串
3.2.1 算法介绍
对于两个长度相等的字符串
对于多个字符串的比较,我们也可以这样做。分成长度对应相等的两部分,以第一部分为第一关键字,第二部分为第二关键字比较。
基于这个思想,我们考虑倍增。
假设已知所有
因为
进行
3.2.2 常数优化与注意点
朴素地实现倍增后缀排序常数太大,以下是常数优化技巧。
-
对第二关键字的排序是不必要的:设当前子串长度为
。若
,则 , 在第二关键字排序中被排到最前面;对于
的所有位置,我们希望按 递增的顺序排列它们,则 在原 中按下标递增的顺序出现。考虑倒推,按下标 从小到大枚举 ,若 ,则加入 。即 在第二关键字排序后的排列,等于所有大于 的 减去 后按下标从小到大排列。 -
计数排序的桶大小:
修改
的定义为在所有 级子串当中小于 的 不同的 子串数量加 。这样有两个好处,一是计数排序的过程中对桶做前缀和时只需枚举到 的最大值,即上一轮的 (注意,尽管最终的 和 互逆,但后缀排序进行的过程中由于相同排名的存在,并不一定满足该性质),减小常数;二是若 已经等于 ,则所有后缀分化完毕,可以直接退出算法而无需等到 。
在实现后缀排序时,还有一些注意点:
-
初始令
,并以 为关键字计数排序得到初始 。 -
每次桶排后根据新的
反推出 :从小到大枚举 ,若 ,则 ,否则 。在此之前需要将原rk
数组拷贝一份到ork
,否则更新rk
时会出错。
给出一份实现良好的后缀排序 模板题 代码,附有部分注释:
#include <bits/stdc++.h>
using namespace std;
constexpr int N = 1e6 + 5;
char s[N];
int n, sa[N], rk[N], ork[N], buc[N], id[N];
void build() {
int m = 1 << 7, p = 0;
for(int i = 1; i <= n; i++) buc[rk[i] = s[i]]++;
for(int i = 1; i <= m; i++) buc[i] += buc[i - 1];
for(int i = n; i; i--) sa[buc[rk[i]]--] = i;
for(int w = 1; ; m = p, p = 0, w <<= 1) { // m 表示桶的大小, 等于上一轮的 rk 最大值.
for(int i = n - w + 1; i <= n; i++) id[++p] = i; // 循环顺序无关, 顺序倒序都可以, 不影响最终结果.
for(int i = 1; i <= n; i++) if(sa[i] > w) id[++p] = sa[i] - w;
memset(buc, 0, m + 1 << 2); // 注意清空桶.
memcpy(ork, rk, n + 1 << 2); // 注意拷贝 rk -> ork.
p = 0;
for(int i = 1; i <= n; i++) buc[rk[i]]++;
for(int i = 1; i <= m; i++) buc[i] += buc[i - 1];
for(int i = n; i; i--) sa[buc[rk[id[i]]]--] = id[i]; // 注意, 倒序枚举保证计数排序的稳定性. 基数排序的正确性基于内层计数排序的稳定性.
for(int i = 1; i <= n; i++) rk[sa[i]] = ork[sa[i - 1]] == ork[sa[i]] && ork[sa[i - 1] + w] == ork[sa[i] + w] ? p : ++p; // 原排名二元组相同则新排名相同, 否则排名 +1.
if(p == n) break; // n 个排名互不相同, 排序完成.
}
}
int main() {
scanf("%s", s + 1);
n = strlen(s + 1);
build();
for(int i = 1; i <= n; i++) printf("%d ", sa[i]);
return 0;
}
- 尽管上述代码中
sa[i - 1] + w
和sa[i] + w
可能达到 ,但ork
的大小只需要开到 :若ork[sa[i - 1]] != ork[sa[i]]
,则程序不会执行第二条判断,直接返回否。否则ork[sa[i - 1]] = ork[sa[i]]
,说明 ,据此可知 和 均 ,因此 和 均 。
3.3 Height 数组
- 定义
表示 与 的最长公共前缀长度 ,即排名为 和 的后缀的 LCP 长度。 未定义,一般为 。
绝大多数 SA 的应用都需要
求
结论 1:若
,则 和 均不小于 。 证明:设
,因为 的字典序在 和 之间,所以 的前 个字符必然和 与 相等。
结论 1 非常容易理解,因为字典序距离越近,LCP 越长。例如,按字典序排序后,两个
若希望求出
假设
令
。 。
它们合起来表达了:存在位置
相信部分读者此时已经想到一些了不起的性质了。
排名为
进一步地,注意到
- 没有讨论
的情况,不过因为 非负所以同样满足上式。 - 因为
为空字符,所以求出的 。
如上图,
根据该性质,我们可以按
下方代码中,
for(int i = 1, k = 0; i <= n; i++) {
if(k) k--;
while(s[i + k] == s[sa[rk[i] - 1] + k]) k++; // sa[rk[i]] = i, 需要保证 s[0] 和 s[n + 1] 为空字符 (多测清空), 否则可能出错.
ht[rk[i]] = k;
}
3.4 应用
3.4.1 任意两个后缀的 LCP
有了
结论 2:设
,则 。 证明:设
。 根据字典序的定义,所有排名在
和 之间的后缀 的前 个字符均与 和 相等。又因为 ,所以存在两个相邻的排名 ,使得 。这样,对于所有 , ,且存在 。
简单地说,
上图为对
如果将整张图逆时针旋转 height
这一名称的来源。正因如此,SA 可以和单调栈相结合:众所周知,单调栈可以求出柱状图中面积最大的矩形。
查询区间最值,使用 ST 表维护即可做到
注意点:
- 查询范围是
而非 。 - 当
时,需要swap(i, j)
。 - 左边界要加
。 - 需特判
的情况。
3.4.2 本质不同子串数
可以用
因为
上述做法求出了
后者对
3.4.3 结合单调栈
例如求所有后缀两两 LCP 长度之和,考虑按排名顺序加入所有后缀并实时维护
对所有
例题:P4248,P7409,CF1073G。
3.4.4 多个串的最长公共子串
给定
令
容易发现随着
双指针部分复杂度线性。
例题:P2463。
3.4.5 结合并查集
注意到两个后缀之间的 LCP 长度以它们排名之间的
从大到小考虑所有
对
例题:P2178,P7361。
3.5 例题
P3763 [TJOI2017] DNA
枚举开始位置并使用 SA 加速匹配。时间复杂度线性对数。代码。
P2852 [USACO06DEC] Milk Patterns G
从大到小添加每个
P2463 [SDOI2008] Sandy 的卡片
差分后求所有
SP220 PHRASES - Relevant Phrases of Annihilation
建出 SA 数组,二分答案,检查每个
P4248 [AHOI2013] 差异
令
将
时间复杂度线性对数,代码。
P7409 SvT
双倍经验。
CF1073G Yet Another LCP Problem
三倍经验。
求出
将
时间复杂度
P4081 [USACO17DEC] Standing Out from the Herd P
将所有字符串用 不同 分隔符连接,后缀排序,对于颜色相同的一段排名区间
这样,这段排名区间对答案的贡献还要减去
时间复杂度
P6640 [BJOI2020] 封印
求出
直接二分答案
*P2178 [NOI2015] 品酒大会
由于
这启发我们求出
进一步地,只记录四个极值就不需要启发式合并 set
,时间复杂度
*CF822E Liar
使用贪心的思想可知在一轮匹配中,我们能匹配尽量匹配,即若从
注意到
设
求一个字符串某两个后缀的 LCP 是后缀数组的拿手好戏,时间复杂度
本题同时用到了贪心,DP 和 SA 这三个跨领域的算法,是好题。
P5028 Annihilate
设排名
枚举每个字符串
因此,从小到大枚举排名
否则
这样避免空间复杂度带
P7769 丑国传说 · 大师选徒(Selecting Apprentices)
考虑
这启发我们把
容易处理第三条限制:符合条件的后缀的排名是一段包含
对于前两条限制,对每个值
时空复杂度均为线性对数。代码。
P2603 [ZJOI2008] 无序运动
这道题的关键在于如何处理两个粒子片段相似。设两粒子片段分别为
容易得知,不考虑翻转时,
用浮点数记录上述信息丢失精度,考虑记录向量长度的平方比,以及向量叉积与点积的比。一个细节,就是叉积比点积得到夹角正切值,但
考虑翻转只需将
因此,我们得到如下算法:用相邻向量之间的信息描述所有片段和粒子运动轨迹。类似字符串匹配,将信息离散化后容易使用 SA 或 AC 自动机求出每个片段在粒子运动轨迹和粒子运动轨迹关于
注意点:
- 特判
。 - 当某个片段翻转后与它本身不考虑翻转相似时,它在原粒子运动轨迹中每出现一次均会在翻转后的粒子运动轨迹的对应位置出现,被重复计算。因此要除以
。
时间复杂度
P6095 [JSOI2015] 串分割
因为字符不含
答案满足可二分性。我们二分答案在后缀数组中的排名。破环成链,枚举
若可匹配
进一步地,比较两个长度为
但其实没有关系,因为若
时间复杂度
*P6793 [SNOI2020] 字符串
对
时间复杂度
P2336 [SCOI2012] 喵星球上的点名
将姓和名用分隔符连接,问题相当于给定
将所有文本串用分隔符连接,建出后缀数组,对每个模式串求出以其为前缀的排名区间。第一问相当于区间不同颜色数,离线扫描线 BIT。第二问相当于对每种颜色查询与其有交的区间数。对每个区间和每个颜色在第一个位置统计答案,则每个位置对其对应的颜色的贡献为左端点落在一段区间,右端点落在另一段区间的区间数量,二维数点,离线扫描线 BIT。
时间复杂度线性对数。代码。
P4143 采集矿石
字典序排名从大到小使得对于固定的
考虑如何求某个子串
满足第一种条件的子串对应后缀
因此,求出排名为
求出
时间复杂度
*CF1654F Minimal String Xoration
非常好题目,爱来自瓷器。
注意到一个重要性质,位运算在每一位独立。
设
因此,类似后缀排序,设
倍增并排序,时间复杂度
*P7361 「JZOI-1」拜神
不错的题目。
建出
显然答案满足可二分性,因此着眼于判断一个长度
考虑如何维护 set
lower_bound
查询
时空复杂度均为线性对数平方。代码。
*P5161 WD 与数列
据定义,差分数组相同的两个串相等。转化为求差分数组不相交且不相邻的相等子串对数量,补集转化得相等子串对数量减去相交或相邻的相等子串对数量。
对差分数组求后缀数组,按
时间复杂度
P5115 Check, Check, Check one two!
看到题目,我首先想到建出正反串 SA 及其
稍微观察一下
对于
理论可以做到关于长度加字符集线性(线性 SA,线性区间 RMQ),但不实用。
听说官方题解是
*P1117 [NOI2016] 优秀的拆分
本题巧妙的地方有两点,一是通过乘法原理将
对于固定的
求出
同理,
综上,我们需要对
求任意两个前缀的 LCS 或任意两个后缀的 LCP 可借助 SA 实现。
时间复杂度线性对数,包括建出 SA,建出
从这道题开始,设置关键点变成了经典套路。
*SP687 REPEATS - Repeats
借用优秀的拆分的套路,直接枚举循环节长度
Sol 1:显然答案满足可二分性。二分
Sol 2:Sol 1 太不优美了。
Sol 3:Sol 2 仍不够优美。根据 border 论最经典结论,
CF1608G Alphabetic Tree
毒瘤细节码农题。
首先,对于一次询问
对于后缀数组,对特定
对于本题也一样。我们先对
具体地,二分排名
求得
- 哈希值的每一位不能直接减去
'a'
,否则aab
和ab
会被视作相等。 - 哈希 base 应大于多串 SA 插入分隔符的最大数值。
- 二分
的下界为 ,上界为 SA 总长加 , 的上下界要减去 。 - 注意分清排名和下标。
*GYM102803E Everybody Lost Somebody
一道考察对 SA 的
对于
接下来考虑
使用倍增桶排求解 SA 的思想,我们知道
对于所有不大于和小于的限制,通过赋边权
接下来考虑加强版
注意到
注意到对于一个
容易将并查集的复杂度去掉,时间复杂度
*CF1043G Speckled Band
易知答案不超过
显然,答案为
若答案为
若答案是
若答案为
若上述条件均不符合,则答案为
最后解决一个遗留问题:求一个子串是否存在 border。当然这可以通过 “border 的四种求法” 的 border 论或 SAM + 树剖解决,但因为只需判断 border 的存在性,所以存在一个优美且巧妙的根号做法。
若 border 相交,则必然形成长度更短的 border。因此我们不妨 钦定 border 不交,得到根号分治做法:若 border 长度小于
时间复杂度
*牛客多校 2022#6L Striking String Problem
给定字符串
,正整数 和 个整数 ,令 。 次询问给定 ,求 在 中的出现次数。
, , , ,时间限制 8s,空间限制 1G。
模拟赛题加强出到牛客多校了,比赛链接。
定义
设
将询问差分,变成
单模式串整体匹配通常使用 KMP,对
朴素算法为暴力匹配
注意到在处理
将问题分成两部分求解,一为计数,二为维护
- 维护
:
问题相当于求
两种情况,
对于后者,只需预处理
对于前者,令
- 第一,
,即 为 的 border。 - 第二,
,即 与 的最长公共前缀不小于 。
考虑条件 1,建出
考虑条件 2,建出
容易发现,只需求得
树上可持久化线段树,
- 计数:
为方便计数,限制
处理
仔细思考后容易发现一次询问相当于求
同样,分成两种情况讨论,
若完全包含,首先
若不完全包含,设出现位置在
因此,类似维护
。 。
不同于维护
查询时,树上倍增找到最深的使得
更新
减小空间常数的方法:问题转化为给定一棵树,每个点有区间
综上,设
*CF917E Upside Down
以
对于直链,相当于
对于跨过 LCA,比较困难。考虑求出
问题转化为求
对于本题可以如法炮制。求
时间复杂度
CHANGE LOG
- 2021.12.12:新增 KMP 算法与 Z 算法。
- 2021.12.13:修改部分笔误。
- 2021.12.23:新增前言。
- 2021.12.24:新增 SA 应用部分。
- 2022.1.10:新增几个 SA 应用与例题。
- 2022.6.10:重构文章,修改表述。
- 2023.1.23:修改表述。
- 2023.2.3:移除 KMP 和 Border 论。
- 2023.4.8:更新后缀排序的介绍。
参考资料
定义
第一章
第二章
第三章
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· 写一个简单的SQL生成工具
· Manus的开源复刻OpenManus初探