1 LCP:从开头连续匹配多长?
LCP 是 Longest Common Prefix 的缩写,意思是最长公共前缀。
例如,比较两个字符串:
第一个:a b c d e f
第二个:a b c x y z
└─abc─┘
从开头开始,前三个字符依次相同,第四个字符不同,所以它们的最长公共前缀是 abc,LCP 长度为 3。
LCP 要求从两个字符串的开头开始,逐个比较相邻字符。遇到不同字符,或其中一个字符串结束,匹配就停止。
1.1 一个字符串中,为什么会出现两个起点?
后缀是从某个位置开始,一直到字符串末尾的部分。
对于同一个字符串
全文使用从 0 开始的下标,字符串长度记为
以 ababa 为例:
下标:0 1 2 3 4
字符:a b a b a
从 0 开始的后缀:ababa
从 2 开始的后缀:aba
公共前缀: aba
因此
再看两个不同的起点组合:
| 起点 |
两个后缀 | LCP 长度 | 停止的原因 |
|---|---|---|---|
ababa、aba |
3 | 第二个后缀结束 | |
baba、ba |
2 | 第二个后缀结束 | |
ababa、a |
1 | 第二个后缀结束 | |
ababa、baba |
0 | 第一个字符就不同 |
LCP 与 [
Rbook: 最长公共子序列] 回答的问题不同。LCP 必须从两个开头开始连续匹配;最长公共子序列允许跳过字符。两个开头不相等时,LCP 直接为 0,不能跳过它们再继续寻找。
2 为什么需要一次求出许多 LCP?
如果只查询一对后缀,直接逐个比较字符即可,最坏需要
但有些问题需要枚举所有起点对。共有 a 时,每次匹配都可能很长。
重复工作在哪里?
比较 ababa 的后缀 s[0..] 和 s[2..] 时,第一对 a 相同。去掉这两个 a,接下来要做的工作,正是比较 s[1..] 和 s[3..]。
所以,不同起点对的比较过程可以共用结果。我们不必每次都把整段字符重新看一遍。
3 用二维 DP 计算 LCP
3.1 状态:固定两个开头
定义:
两个下标分别记录两个后缀的起点。这是一个二维状态。
为了表示后缀走到末尾的情况,再约定位置
只要其中一个后缀已经为空,公共前缀长度就是 0。
3.2 转移:先比较第一对字符
当
:第一对字符就不同,公共前缀长度为 0。 :这一对贡献长度 1,剩余部分的长度是 。
于是:
这里不需要在多个选择之间取最大值。两个起点已经固定,第一对字符能否匹配也已经确定;能够匹配,就继续比较下一对,不能匹配就停止。
为什么相等时恰好是
3.3 顺序:先算右下方,再算当前位置
状态依赖为:
放到表格里看,当前位置需要的是右下方的格子。因此可以让
for (int i = n - 1; i >= 0; i--) {
for (int j = n - 1; j >= 0; j--) {
if (s[i] == s[j]) {
lcp[i][j] = lcp[i + 1][j + 1] + 1;
} else {
lcp[i][j] = 0;
}
}
}
代码执行前,需要准备一个至少为
这里两个循环都倒序,是为了直观体现依赖关系。实际上,只要保证第
3.4 手算 ababa
先沿着依赖链看
lcp[0][2]:a == a,需要 1 + lcp[1][3]
lcp[1][3]:b == b,需要 1 + lcp[2][4]
lcp[2][4]:a == a,需要 1 + lcp[3][5]
lcp[3][5]:第二个后缀为空,值为 0
实际填表时,顺序反过来:
完整表格如下,最后一行和最后一列是空后缀边界:
| 0:a | 1:b | 2:a | 3:b | 4:a | 5:空 | |
|---|---|---|---|---|---|---|
| 0:a | 5 | 0 | 3 | 0 | 1 | 0 |
| 1:b | 0 | 4 | 0 | 2 | 0 | 0 |
| 2:a | 3 | 0 | 3 | 0 | 1 | 0 |
| 3:b | 0 | 2 | 0 | 2 | 0 | 0 |
| 4:a | 1 | 0 | 1 | 0 | 1 | 0 |
| 5:空 | 0 | 0 | 0 | 0 | 0 | 0 |
表格还有两个可以自查的性质:
共有
4 应用:ABC141 E 的不重叠重复子串
题目链接:[
atcoder abc141_e: Who Says a Pun?]
给定字符串,求一个最长的非空字符串,使它在原串中至少出现两次,而且选出的两次出现不能重叠。不存在时,答案为 0。
注意答案是重复部分本身的长度。例如 abcabc 的答案是 3,对应两次 abc。
4.1 先固定两个起点
设两次出现的起点为
第一条限制是两个子串相等。它们必须在从
第二条限制是不能重叠。第一个子串占据
将两个上界合在一起,固定这对起点时的最大合法长度就是:
这个值不仅是上界,也一定可以取到:它没有超过公共前缀长度,所以两段相等;它没有超过起点距离,所以两段不重叠。值为 0 时,表示这对起点无法提供非空答案。
最后枚举全部起点对:
4.2 为什么 ababa 的答案不是 3?
对于 aba 占据的区间分别是
把长度限制为 2,则两个 ab 占据
| 起点 |
LCP 长度 | 起点距离 |
最大合法长度 |
|---|---|---|---|
| 3 | 2 | 2 | |
| 2 | 2 | 2 | |
| 1 | 4 | 1 | |
| 0 | 1 | 0 |
LCP 负责限制“能够相等多长”,起点距离负责限制“多长才不会重叠”。两个限制都必须保留。
4.3 完整 C++17 代码
先计算完整 LCP 表,再枚举起点对,代码中的两个步骤分别对应上面的两个问题。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 5005;
// 全局数组自动初始化为 0,第 n 行和第 n 列表示空后缀。
int lcp[maxn][maxn];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
string s;
cin >> n >> s;
// lcp[i][j]:从 i、j 开始的两个后缀的最长公共前缀长度。
// 当前状态依赖右下方的 lcp[i + 1][j + 1],因此从后向前计算。
for (int i = n - 1; i >= 0; i--) {
for (int j = n - 1; j >= 0; j--) {
if (s[i] == s[j]) {
lcp[i][j] = lcp[i + 1][j + 1] + 1;
} else {
lcp[i][j] = 0;
}
}
}
int ans = 0;
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
// 相等限制长度不超过 lcp[i][j],不重叠限制长度不超过 j - i。
int len = min(lcp[i][j], j - i);
ans = max(ans, len);
}
}
cout << ans << '\n';
return 0;
}
官方样例:
输入:
5
ababa
输出:
2
另外两个官方样例 xy、strangeorange 的答案分别为 0、5。后者的两个 range 分别从位置 2 和 8 开始。
题目限制为 lcp 放在全局,避免把大型二维数组放进栈;全局数组也会自动初始化为 0。
按每个 int 占 4 字节计算,5005 × 5005 的数组约占 100.2 MB,即 95.6 MiB,符合本题内存限制。
填表和枚举起点对都需要
5 和连续段 DP 有什么关系?
前一篇 [
Rbook: 连续段 DP:为什么要以当前位置结尾] 讲过:固定距离
ABC141 E 也能这样做。这里没有问号,两个字符必须直接相等。
5.1 固定距离,就是沿着 LCP 表的一条对角线
固定
这些位置对在二维表中,恰好位于同一条从左上到右下的对角线上。
以 ababa、
| 左侧位置 |
0 | 1 | 2 |
|---|---|---|---|
| 比较的字符对 | (a, a) |
(b, b) |
(a, a) |
| 从左到右算:以当前位置结尾的连续匹配对数 | 1 | 2 | 3 |
| 从右到左算: |
3 | 2 | 1 |
它们描述的是同一段匹配关系,但状态的端点不同:
- 连续计数固定的是结尾,所以依赖前一对,扫描时向前累加。
- LCP 固定的是开头,所以依赖后一对,填表时从后向前累加。
不要把同一位置的 cnt 和 lcp[i][i+d] 当成同一个值。例如表中
5.2 连续匹配对数如何变成不重叠答案?
设扫描到左侧位置 cnt。若从这段末尾取出
它们不重叠,要求第一段的结尾在第二段的起点之前,即:
同时,能够取出的字符对数不能超过 cnt,所以当前可以提供的最大合法长度为:
枚举所有距离,并扫描该距离下的全部位置,就覆盖了所有可能的子串对。对于任意一对长度为
对应的完整实现为:
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
string s;
cin >> n >> s;
int ans = 0;
for (int d = 1; d < n; d++) {
int cnt = 0; // 当前距离下,以位置 i 结尾的连续匹配对数。
for (int i = 0; i + d < n; i++) {
if (s[i] == s[i + d]) {
cnt++;
} else {
cnt = 0;
}
// 取当前匹配段末尾的 len 对,不重叠要求 len <= d。
int len = min(cnt, d);
ans = max(ans, len);
}
}
cout << ans << '\n';
return 0;
}
注意这里的 abcxabc 的两次 abc 相距 4,虽然答案长度只有 3,仍然需要检查
| 比较项目 | 二维 LCP DP | 固定距离 + 连续计数 |
|---|---|---|
| 如何组织字符对 | 按两个起点填二维表 | 按起点距离逐条扫描对角线 |
| 状态含义 | 从两个起点开始的连续匹配长度 | 截至当前位置的连续匹配对数 |
| 依赖方向 | 右下方的下一个位置对 | 左上方的前一个位置对 |
| 不重叠限制 | ||
| 时间 | ||
| 额外空间 |
二维 LCP 表可以保留任意起点对的查询结果;固定距离计数在本题中只需维护全局答案,因此不必保存整张表。
5.3 与 CF1948D 的条件有什么区别?
两道题都可以固定距离、统计连续匹配,但对匹配段的要求不同。
| 条件 | CF1948D | ABC141 E |
|---|---|---|
| 字符匹配 | 相等,或至少一个是问号 | 必须相等 |
| 两段的位置关系 | 两半必须相邻 | 两次出现不能重叠,允许有空隙 |
| 固定距离 |
需要连续至少 |
可以取不超过 |
| 对答案的贡献 | 整个重复串长度 |
单次出现的长度 |
因此,CF1948D 中“达到
6 三个自查练习
6.1 练习一:LCP 很长,合法长度也一定很长吗?
设 s = "aaaaaa",分别计算起点对
点击
对于
对于
全局答案为 3,对应两次 aaa。任意两段长度为
6.2 练习二:为什么遇到不相等的字符必须归零?
设 s = "xabzab"。虽然两个起点后面都出现了 ab,为什么
点击
从位置 0 和 3 开始的后缀分别是 xabzab、zab。第一对字符 x 与 z 不同,因此公共前缀为空,LCP 长度为 0。
后面的 ab 可以由另一对起点查询:
6.3 练习三:只看当前匹配段的总长度够吗?
在 ababa 中固定
点击
长度为 3 的两段是位置 aba 和位置 aba,它们重叠。距离只有 2,要避免重叠,所取长度也必须不超过 2。
因此使用
7 进一步阅读
本文计算的是所有后缀对的 LCP,使用二维 DP 即可完成。后缀数组相关资料中的“LCP 数组”通常只保存按字典序排列后相邻后缀的 LCP,数据的组织方式与这里的二维表不同。
现阶段先掌握状态含义、逆序依赖和不重叠约束即可。本题不需要后缀数组、字符串哈希或 KMP。