题单 · 习题解析

洛谷 P2375 [NOI2014] 动物园

★★★ 难全在第一步翻译上:「**这个后缀与这个前缀不重叠**」⟺「长度 ≤ 一半」(前缀占 `[0,b)`、后缀占 `[L-b,L)` ⇒ 不重叠 ⟺ `b ≤ L/2`);再拆两半,每半一句话 —— ① **border 的 border 还是 border** ⇒ `cnt[i] = cnt[nxt[i]-1] + 1`(★ 整个 `nxt` 数组其实是一棵树);② 「≤ 一半」那个限制只要再养一个**均摊指针** `k`;⇒ 一趟 `O(L)`;★★★ 而这一页最值钱的是那张对拍表的第一行:**顺手写的那一档同时打出三个 0,而三个 0 是三件不同的事** —— 「漏掉不重叠」是**概率低**(3000 轮才抓到 **5** 次,换档位到 2 个字母立刻 139)/「ans 没每组重置」是**结构性**(那一档只造一组数据,加到三万轮也不会现形)/「每轮从头跳」是**答案永远对**(对拍原理上看不见,只能换尺子数次数)⇒ **看到一整行 0,先一个一个问它是哪一种**;★★★ 那个慢法的次数表:全 `a` 串上 249 500 / 3 998 000 / **63 992 000**(n 每翻 4 倍 **×16.0**,而正解是 ×4.0),`n = 16000` 上差 **8000 倍**,⚠ 而随机串那一列是**精确的 0**;★★ 秒表把那堵墙钉在测试点 5 和 6 之间(`L = 10⁴` **0.04 秒** / `L = 10⁵` **4.37 秒**)⇒ **它稳拿 50 分**;★★ 「多测」本身就是一个档位 —— 题面第一行写着组数,生成器就必须真的造多组;★ 八格「触发 ≡ 抓获」一个不差;★ 官方三组样例把两个「答案错」的**全打死**(和[第 47 章 P1071](/sol/p1071/) 那次正好相反),而对慢法完全无能为力

原题:洛谷 P2375出自 第 48 章 KMP:失配的时候,i 一步都不用退 的题单题面本地存档:2026-09-08
⚠ 先自己写一遍,再往下看

这一页不是标准答案,是一条阶梯:把大多数人真实会写出的第一版摆出来, 看它错在哪、慢在哪,再一步一步改。所以它对「已经自己动手撞过一次墙」的人最有用。
还没写过的话,先去写 —— 写出来的哪怕是错的,也比直接看这一页收获大。

0题目原文(本地存了一份)

为什么要在这儿抄一份题面

原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。

转录自洛谷 P2375,日期见页头。两边不一致时信原站。

题目描述

园长给动物们讲解 KMP 算法。

熊猫:「对于字符串 S 的前 i 个字符构成的子串,既是它的后缀又是它的前缀的字符串中(它本身除外), 最长的长度记作 next[i]。」

例:Sabcababc,则 next[5] = 2。同理 next[1] = next[2] = next[3] = 0next[4] = next[6] = 1next[7] = 2next[8] = 3

下课前,园长提出了一个问题:

「我现在希望求出一个更强大的 num 数组 —— 对于字符串 S 的前 i 个字符构成的子串, 既是它的后缀同时又是它的前缀,并且该后缀与该前缀不重叠,将这种字符串的数量记作 num[i]

例如 Saaaaa,则 num[4] = 2。这是因为 S 的前 4 个字符为 aaaa,其中 aaa 都满足性质『既是后缀又是前缀』,同时保证这个后缀与这个前缀不重叠。 aaa 虽然满足性质『既是后缀又是前缀』,但遗憾的是这个后缀与这个前缀重叠了,所以不能计算在内。 同理,num[1] = 0num[2] = num[3] = 1num[5] = 2。」

特别地,为了避免大量的输出,你不需要输出 num[i] 分别是多少, 你只需要输出所有 (num[i]+1) 的乘积,对 10⁹ + 7 取模的结果即可。

输入格式

第 1 行仅包含一个正整数 n,表示测试数据的组数。随后 n 行,每行一个字符串 S(仅含小写字母)。

输出格式

包含 n 行,每行一个整数,表示这组测试数据的答案对 10⁹+7 取模的结果。

数据范围

测试点 约定 测试点 约定
1 n ≤ 5, L ≤ 50 6 n ≤ 5, L ≤ 100,000
2、3 n ≤ 5, L ≤ 200 7 n ≤ 5, L ≤ 200,000
4、5 n ≤ 5, L ≤ 10,000 8 n ≤ 5, L ≤ 500,000
9、10 n ≤ 5, L ≤ 1,000,000

时限 1 秒,内存 524288 KB(512 MB)。

输入输出样例

输入

3
aaaaa
ab
abcababc

输出

36
1
32

第一组 aaaaanum0 1 1 2 2 ⇒ 乘积 1×2×2×3×3 = 36

★ 注意第二组 ab 的答案是 1 —— 它一个 border 都没有,(0+1)×(0+1) = 1

1★★ 先把题面翻译成一句话:num[i] = 长度 ≤ i/2 的非空 border 个数

★ 「不重叠」就是「长度不超过一半」

一个既是前缀又是后缀的串,长度 b,放在长度 L 的串里: 前缀占 [0, b)、后缀占 [L-b, L)。⇒ 两段不重叠 ⟺ b ≤ L − bb ≤ L/2

num[i] = s[0..i-1]长度 ≤ i/2 的非空 border 有几个。 题面自己给的例子正好对上:aaaa 的 border 是 1、2、3,其中 ≤ 2 的有两个 ⇒ num[4] = 2

★★ 拆成两半,每一半都只要一句话

① border 链的长度能递推。cnt[i] = s[0..i] 的非空 border 个数:

   cnt[i] = nxt[i] ? cnt[nxt[i]-1] + 1 : 0;

—— 因为「border 的 border 还是 border」,一个前缀的所有 border 正好排成一条链, 而这条链的下一节就是 nxt[i] 那个前缀的链。 (★ 第 48 章第 5 步nxt 是「拿 p 去匹配 p」;这里再进一步: 整个 nxt 数组其实是一棵树,第 i 个点的父亲是 nxt[i]

② 「长度 ≤ 一半」那个限制,再养一个指针就够。k 像 KMP 一样跟着 i 往前走,走完再缩回来:

   while (k > 0 && s[i] != s[k]) k = nxt[k-1];
   if (s[i] == s[k]) k++;
   while (k > 0 && 2*k > i+1) k = nxt[k-1];      // ★ 缩到不重叠为止
   num = k ? cnt[k-1] + 1 : 0;

k 每轮最多 +1、每次回退至少 −1 且不为负 ⇒ 回退总数均摊 O(L),整体一趟 O(L)。 ⚠ 而所有人的第一版都是「每个 inxt[i] 重新往下跳」—— 那是 O(L²),第 ④ 步量它。

p2375.cpp★ 正解:两个指针,一趟 O(L)
// P2375 [NOI2014] 动物园 —— 正解:两个指针,一趟 O(L)
//
// ★★ 题面要的 num[i]:s 的前 i 个字符里,「既是前缀又是后缀、**而且两段不重叠**」的串有几个。
// 翻译过来就是:**长度 ≤ i/2 的非空 border 有几个。**
//
// ★ 拆成两半,每一半都只要一句话:
// ① **border 链的长度**能递推:cnt[i] = s[0..i] 的非空 border 个数
// = cnt[nxt[i]-1] + 1(nxt[i] > 0 时),否则 0。
// —— 因为「border 的 border 还是 border」,整条链就是一棵树上的一条路径。
// ② **「长度 ≤ 一半」那个限制**只要再养一个指针 k:它像 KMP 一样跟着 i 往前走,
// 走完再 `while (2k > i+1) k = nxt[k-1]` 缩回来。
// ⇒ k 只增不减地往前走、每次回退至少减一 ⇒ **两个指针都是均摊 O(1)**,整体 O(L)。
// 最后 num[i] = k > 0 ? cnt[k-1] + 1 : 0(长度 ≤ k 的 border 就是「k 自己 + k 那个前缀的全部 border」)。
//
// ⚠ 三个容易漏的角:
// · 「不重叠」那半漏掉 ⇒ 变成 cnt[i](见 p2375Overlap.cpp);
// · 那个 k 不用均摊指针、每个 i 都从头跳 ⇒ O(L²),**答案对但跑不完**(见 p2375Chain.cpp);
// · **多测**:答案累乘的变量每组都要重置(见 p2375Ans.cpp)。
#include <cstdio>
#include <cstring>
using namespace std;
static const int MAXN = 1000006;
static const long long MOD = 1000000007LL;
static char s[MAXN];
static int nxt[MAXN], cnt[MAXN];
int main() {
int T;
if (scanf("%d", &T) != 1) return 0;
while (T--) {
if (scanf("%s", s) != 1) return 0;
int n = (int)strlen(s);
nxt[0] = 0;
cnt[0] = 0;
for (int i = 1, j = 0; i < n; i++) {
while (j > 0 && s[i] != s[j]) j = nxt[j - 1];
if (s[i] == s[j]) j++;
nxt[i] = j;
cnt[i] = j ? cnt[j - 1] + 1 : 0;
}
long long ans = 1; // ⚠ 每组都要重置
for (int i = 1, k = 0; i < n; i++) {
while (k > 0 && s[i] != s[k]) k = nxt[k - 1];
if (s[i] == s[k]) k++;
while (k > 0 && 2 * k > i + 1) k = nxt[k - 1]; // ★ 缩到「不重叠」为止
long long num = k ? cnt[k - 1] + 1 : 0;
ans = ans * ((num + 1) % MOD) % MOD;
}
printf("%lld\n", ans);
}
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
p2375Brute.cpp★ 参照物:按题面的定义逐个前缀、逐个长度硬数
★ 那一层翻译是验过的

参照物不碰 next、不提 border 链:对每个前缀,把 b = 1..L/2 挨个试 「前 b 个字符 == 后 b 个字符吗」。⇒ 四档 1200 轮,和正解 0 组不一致。 ★ 它验的是「不重叠 ⟺ b ≤ L/2」和「num[i] = cnt[k-1]+1」这两步翻译。

2⚠ 错法一:漏掉了「不重叠」

★ 触发条件:存在某个前缀,它的最长 border 超过了一半

题面把「不重叠」说了两遍,还专门举了反例(aaaa 里的 aaa 不算)。 漏掉这半句,num[i] 就变成了「所有非空 border 的个数」= cnt[i]

★ 触发条件:存在 i 使 2 × nxt[i] > i+1 —— 也就是某个前缀「自己叠在自己身上」。 ⇒ 官方样例第一组(aaaaa)一测就死:36 → 120。 ⚠ 而顺手写的随机数据几乎碰不到它(第 ⑤ 步:26 个字母随机,3000 轮才抓到 5 次)。

p2375Overlap.cpp✗ 错法一:算成了「所有 border」
// P2375 ✗ 错法一:漏掉了「这个后缀与这个前缀**不重叠**」
//
// ⚠ 题面把这句话说了两遍,还专门举了例子:`S = aaaaa` 时 `num[4] = 2`,
// 因为 `aaa` 虽然既是前缀又是后缀,**但两段重叠了**,不算。
// ⇒ 漏掉这半句,num[i] 就变成了「**所有**非空 border 的个数」= cnt[i]。
// ★ 触发条件:**存在某个前缀,它有长度 > 一半的 border**
// —— 也就是那个前缀「自己叠在自己身上」。⇒ 官方样例第一组(`aaaaa`)一测就死。
#include <cstdio>
#include <cstring>
using namespace std;
static const int MAXN = 1000006;
static const long long MOD = 1000000007LL;
static char s[MAXN];
static int nxt[MAXN], cnt[MAXN];
int main() {
int T;
if (scanf("%d", &T) != 1) return 0;
while (T--) {
if (scanf("%s", s) != 1) return 0;
int n = (int)strlen(s);
nxt[0] = 0; cnt[0] = 0;
for (int i = 1, j = 0; i < n; i++) {
while (j > 0 && s[i] != s[j]) j = nxt[j - 1];
if (s[i] == s[j]) j++;
nxt[i] = j;
cnt[i] = j ? cnt[j - 1] + 1 : 0;
}
long long ans = 1;
for (int i = 1; i < n; i++) {
long long num = cnt[i]; // ⚠ 所有 border,没管重不重叠
ans = ans * ((num + 1) % MOD) % MOD;
}
printf("%lld\n", ans);
}
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

3⚠ 错法二:多测,可答案那个变量忘了每组重置

★ 和算法一点关系都没有,而它的触发条件是一句话

题面第一行是组数 nn ≤ 5),而答案是累乘出来的。 把 long long ans = 1; 写在 while (T--) 外面,第二组就带着第一组的积开始乘。

★ 触发条件:组数 ≥ 2,而且前面某一组的答案不是 1。 ⇒ 官方样例三组(36 / 1 / 32)一测就死:它打出 36 / 36 / 1152

p2375Ans.cpp✗ 错法二:ans 没每组重置

4★★★ 慢法:那个指针每轮从头跳 —— 答案永远对,只能数次数

p2375Count.cpp★ 换尺子:数「k 一共回退了多少步」
// P2375 解析页上那几个「对拍看不见」的数字的出处。
// ./p2375Count 人话版
// ./p2375Count csv 给 check:viz 用
//
// ★★ 这道题唯一一个对拍抓不到的错法是「那个 k 每轮都从头跳」——它**答案永远对**,
// ⇒ 只能[换尺子数次数](/ch/48-kmp/)。这里数的是「k 一共回退了多少步」:
// · 正解:k 只增不减地往前走,回退总数**均摊 O(L)**;
// · 跳链版:每个 i 都从 nxt[i] 重新往下跳 ⇒ 全是同一个字母时是 **O(L²)**。
// ⚠ 而随机串上两者几乎一样 —— **顺手造一组顶格随机跑一遍,这个坑一步都看不见。**
#include <bits/stdc++.h>
using namespace std;
static void build(const string& s, vector<int>& nxt) {
int n = (int)s.size();
nxt.assign(n, 0);
for (int i = 1, j = 0; i < n; i++) {
while (j > 0 && s[i] != s[j]) j = nxt[j - 1];
if (s[i] == s[j]) j++;
nxt[i] = j;
}
}
/** 正解那个均摊指针:一共回退多少步 */
static long long stepsAmortized(const string& s) {
int n = (int)s.size();
vector<int> nxt;
build(s, nxt);
long long steps = 0;
for (int i = 1, k = 0; i < n; i++) {
while (k > 0 && s[i] != s[k]) { k = nxt[k - 1]; steps++; }
if (s[i] == s[k]) k++;
while (k > 0 && 2 * k > i + 1) { k = nxt[k - 1]; steps++; }
}
return steps;
}
/** 跳链版:每个 i 都从 nxt[i] 重新往下跳 */
static long long stepsRestart(const string& s) {
int n = (int)s.size();
vector<int> nxt;
build(s, nxt);
long long steps = 0;
for (int i = 1; i < n; i++) {
int k = nxt[i];
while (k > 0 && 2 * k > i + 1) { k = nxt[k - 1]; steps++; }
}
return steps;
}
int main(int argc, char** argv) {
bool csv = argc > 1 && string(argv[1]) == "csv";
mt19937 rng(20260908u);
vector<int> NS = {1000, 4000, 16000};
vector<long long> amoA, resA, resR;
for (int n : NS) {
string a((size_t)n, 'a');
amoA.push_back(stepsAmortized(a));
resA.push_back(stepsRestart(a));
string r;
for (int i = 0; i < n; i++) { unsigned c = rng() % 26u; r.push_back((char)('a' + c)); }
resR.push_back(stepsRestart(r));
}
/* 顶格输入有多大:5 组 × 10⁶ 个字符 */
long long inBytes = 5LL * 1000000 + 5 + 2;
if (csv) {
for (size_t i = 0; i < NS.size(); i++) printf("amoA%d,%lld\n", NS[(int)i], amoA[i]);
for (size_t i = 0; i < NS.size(); i++) printf("resA%d,%lld\n", NS[(int)i], resA[i]);
for (size_t i = 0; i < NS.size(); i++) printf("resR%d,%lld\n", NS[(int)i], resR[i]);
printf("ratioRes1,%.1f\nratioRes2,%.1f\n",
(double)resA[1] / (double)resA[0], (double)resA[2] / (double)resA[1]);
printf("ratioAmo1,%.1f\nratioAmo2,%.1f\n",
(double)amoA[1] / (double)amoA[0], (double)amoA[2] / (double)amoA[1]);
printf("inBytes,%lld\n", inBytes);
return 0;
}
printf("「k 一共回退了多少步」(全是 a 的串 / 随机 26 个字母):\n");
printf(" %8s %14s %14s %14s\n", "n", "正解(均摊)", "跳链版(全a)", "跳链版(随机)");
for (size_t i = 0; i < NS.size(); i++)
printf(" %8d %14lld %14lld %14lld\n", NS[(int)i], amoA[i], resA[i], resR[i]);
printf(" n 每翻 4 倍:正解 ×%.1f ×%.1f(线性);跳链版 ×%.1f ×%.1f(★ O(n²) 的签名)\n",
(double)amoA[1] / (double)amoA[0], (double)amoA[2] / (double)amoA[1],
(double)resA[1] / (double)resA[0], (double)resA[2] / (double)resA[1]);
printf("⇒ 顶格 L = 10⁶ 时跳链版约 2.5 × 10¹¹ 步;而顶格输入本身有 %lld 字节(5 组 × 10⁶)\n", inBytes);
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果
★★ 正解和它只差一处:k 是不是每轮都重新开始
n(全是 a ★ 正解(均摊指针) ⚠ 每轮从头跳 随机 26 个字母(从头跳)
1 000 499 249 500 0
4 000 1 999 3 998 000 0
16 000 7 999 63 992 000 0
倍数(n 每翻 4 倍) ×4.0(线性) ×16.0O(n²) 的签名)

n = 16000 上两者差 8000 倍;顶格 L = 10⁶ 时「从头跳」约 2.5 × 10¹¹ 步。

秒表(A 机:WSL2 6.18 / 8 核 / 8 GB,2026-09-08,独占;单组全 a):

L 正解 ⚠ 每轮从头跳
10⁴ < 0.01 秒 0.04 秒
10⁵ < 0.01 秒 4.37 秒(时限 1 秒)

⇒ ★★ 那堵墙正好落在测试点 5 和 6 之间L ≤ 10⁴L ≤ 10⁵) ⇒ 「每轮从头跳」那一版稳拿 50 分(测试点 1~5)。 ⚠⚠ 而随机串那一列是精确的 0:随机 26 个字母几乎没有 border ⇒ 顺手造一组顶格随机跑一遍,这个坑一步都看不见。

p2375Chain.cpp⚠ 慢法:答案全对,O(L²)

5★★★ 对拍:顺手写的那一档同时打出两个 0,而两个 0 一个救得回来、一个救不回来

p2375Gen.cpp★ 生成器:只有档 3 才造多组数据
// P2375 的生成器:./p2375Gen 种子 [档位]
//
// ★ 三个待测版本各靠什么现形:
// · p2375Overlap(漏掉「不重叠」)→ **存在某个前缀,它的最长 border 超过了一半**
// (也就是 `2 × nxt[i] > i+1`);
// · p2375Ans(答案没每组重置) → **组数 ≥ 2,而且前面某组的答案不是 1**;
// · p2375Chain(k 每轮从头跳) → ⚠ **答案永远对**,对拍原理上看不见。
//
// 档位:
// 0 ★ 顺手写的:**一组**,26 个小写字母随机,L = 10~40
// ⚠ 这一档同时把两个错法打成 0:随机串几乎没有 border(Overlap 抓不到),
// 而且**只有一组**(Ans 结构上不可能现形)
// 1 ⚠ 字母表压到 2 个
// 2 ⚠ 周期串:一小段重复若干次 ⇒ 长 border 满地都是
// 3 ★ 最终档:**2~5 组** + 周期串
//
// ⚠ rng() 一律先落到具名变量再传参([第 24 章 P1776](/sol/p1776/) 那一跤)。
#include <bits/stdc++.h>
using namespace std;
int main(int argc, char** argv) {
unsigned seed = argc > 1 ? (unsigned)atoi(argv[1]) : 1;
int mode = argc > 2 ? atoi(argv[2]) : 0;
mt19937 rng(seed * 1000003u + 20260908u);
int alpha = (mode >= 1) ? 2 : 26;
auto pick = [&]() { unsigned r = rng() % (unsigned)alpha; return (char)('a' + r); };
int T = 1;
if (mode == 3) { unsigned rt = rng() % 4u; T = 2 + (int)rt; }
printf("%d\n", T);
for (int t = 0; t < T; t++) {
string s;
if (mode >= 2) {
unsigned rd = rng() % 3u;
int d = 1 + (int)rd;
unsigned rk = rng() % 8u;
int k = 3 + (int)rk;
string unit;
for (int i = 0; i < d; i++) unit.push_back(pick());
for (int i = 0; i < k; i++) s += unit;
} else {
unsigned r = rng() % 31u;
int n = 10 + (int)r;
for (int i = 0; i < n; i++) s.push_back(pick());
}
printf("%s\n", s.c_str());
}
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果

四档 × 300 轮(正解 vs 按定义暴力:1200 轮 0 组不一致):

档位 ✗ 漏掉不重叠 ✗ ans 没重置 ⚠ 每轮从头跳
0 ★ 顺手写的(一组,26 个字母随机) 0 0 0
1 ⚠ 字母表压到 2 个 139 0 0
2 ⚠ 周期串(一段重复 3~10 次) 300 0 0
3 ★ 最终档(2~5 组 + 周期串) 300 300 0
★★★ 三条读得出来的结论
  1. ★★★ 档 0 那一行有三个 0,而三个 0 是三件不同的事

    那个 0 是什么 怎么分辨 / 怎么救
    ✗ 漏掉不重叠 概率低 ★ 把轮数加深到 3000 轮 ⇒ 抓到 5 次;真正的救法是换档位(字母表压到 2 ⇒ 139)
    ✗ ans 没重置 结构性 那一档只有一组数据 ⇒ 加到三万轮也不会现形,只能改生成器
    ⚠ 每轮从头跳 答案永远对 对拍原理上看不见 ⇒ 换尺子数次数(第 ④ 步)

    ⇒ ★★ 第 23 章 P1049 那条「两种 0,加深轮数就能分开」在这一页凑齐了三种。 ⚠ 看到一整行 0,先一个一个问它是哪一种 —— 三种的救法完全不同。

  2. ★★ 「多测」这件事本身就是一个档位 —— 前三档都只造一组数据, 于是「ans 没重置」那一列结构性地全是 0。 ⇒ 题面第一行写着组数,生成器就必须真的造多组(顺手写的那种「反正只测一组」会漏掉一整类 bug)。

  3. 八格「触发 ≡ 抓获」一个不差2 × nxt[i] > i+1 /「组数 ≥ 2 且前面某组答案 ≠ 1」。

6★ 官方那三组样例:把两个「答案错」的都打死了

★★ 而它对第三个(慢法)完全无能为力
✗ 漏掉不重叠 ✗ ans 没重置 ⚠ 每轮从头跳
官方样例(三组) (36 → 120) (36 / 36 / 1152) 放过

★ 出题人给的三组一组比一组会问aaaaa 专问「不重叠」、ab 专问「一个 border 都没有」、 abcababc 是正常的一组 —— ⚠ 而三组放在一起才问得出「ans 有没有重置」。 ⇒ ★ 这和第 47 章 P1071 那次正好相反:那道题的三组样例分别对应三种终止状态, 却仍然放过了最深的那个错法;这道题的三组是全打死

⚠ 而它们对「每轮从头跳」完全无能为力 —— 那一版的答案是对的 (第 20 章 P5019 那条:样例这个过滤器筛的是「答案错」)。

7★ 哪一版就已经能过了

★ 这道题是提高组+,可它的三关一关比一关朴素
版本 结果 说明
p2375.cpp AC 两个指针一趟 O(L);顶格输入 5 MB(5 组 × 10⁶)
p2375Chain.cpp 50 分 测试点 1~5(L ≤ 10⁴,0.04 秒);L = 10⁵ 就要 4.37 秒
✗ 漏掉不重叠 WA 样例就死;⚠ 顺手写的对拍 3000 轮才抓 5 次
✗ ans 没重置 WA 样例就死;⚠ 只造一组数据的对拍结构上抓不到

⇒ ★★ 一句话带走:这道题的「难」全在第一步翻译上 —— 把「不重叠」读成「长度 ≤ 一半」,把「border 的 border 还是 border」用成一条递推。 ⇒ 而剩下两个坑(多测重置、均摊指针)和 NOI 没有关系, 它们在任何一道多测的题、任何一处 while 回退里都会再出现一次。