题单 · 习题解析

洛谷 P1470 [IOI 1996 / USACO2.3] 最长前缀

★★★ 这道题是第 48 章的**对照组** —— 它长得很像字符串题(大写字母、前缀、匹配),可两个坑**和 KMP 一点关系都没有**:① **读入**(元素集合「可能不止一行」、`S`「一行或者多行」);② **`f` 不是单调的** ⇒ 要的是「**最大的** k」而不是「第一个断点之前的 k」,最小反例只要一个元素(`P = {AA}`、`S = AAAA` ⇒ `f` 是 **F T F T**,答案 4 而「遇假就停」输出 0);⇒ 学完一个工具之后,第一件该做的不是找地方用它,是**先问这道题问的到底是什么**;★★★ 而这一页最值钱的一条:**官方样例和顺手写的对拍会一起漏掉同一格** —— 样例的 `S` 正好只有一行,顺手写的生成器也只造一行 ⇒ 「以为 S 只有一行」那个错法在样例上和对拍前两档**全是 0**([第 47 章 P1308](/sol/p1308/) 那条的又一次);★★ 「200 个元素挨个试、4 亿次肯定超时」**又一次被打回**:两句短路(`!f[k-len]` continue + 一撞上就 break)把它砍掉一大截,顺手顶格只做 **2 700 909** 次字符比较,★ 而专门卡它的形状(元素共享前缀 `AAAAA`、唯一能匹配的排最后、`S` 全是 A)是 **238 989 254** 次 —— **差 88.5 倍**,可秒表也只有 **0.10 秒 / 时限 1 秒** ⇒ 顺手造那一档连一成力气都没使出来([「顶格 ≠ 最坏」](/sol/p3916/)又一次);★ 正解按长度分组 + 哈希,两种形状都是 **20 万次**查表;★ 十二格「触发 ≡ 抓获」一个不差;★ 「元素长度全 ≥ 2」是「洞」的开关(60 / 300 → 300 / 300)

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

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

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

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

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

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

题目描述

在生物学中,一些生物的结构是用包含其要素的大写字母序列来表示的。

如果一个集合 P 中的元素可以串起来(元素可以重复使用)组成一个序列 S, 那么我们认为序列 S 可以分解为 P 中的元素。元素不一定要全部出现 (如下例中 BBC 就没有出现)。举个例子,序列 ABABACABAAB 可以分解为下面集合中的元素: {A, AB, BA, CA, BBC}

序列 S 的前面 k 个字符称作 S 中长度为 k 的前缀。设计一个程序,输入一个元素集合以及一个 大写字母序列,设 S′ 是序列 S 的前缀,使其可以分解为给出的集合 P 中的元素, 求 S′ 的长度 k最大值

输入格式

输入数据的开头包括若干个元素组成的集合 P,用连续的以空格分开的字符串表示。 字母全部是大写,数据可能不止一行。元素集合结束的标志是一个只包含一个 . 的行, 集合中的元素没有重复。

接着是大写字母序列 S用一行或者多行的字符串来表示,每行不超过 76 个字符。 换行符并不是序列 S 的一部分。

输出格式

只有一行,输出一个整数,表示 S 符合条件的前缀的最大长度。

数据范围

对于 100% 的数据,1 ≤ card(P) ≤ 2001 ≤ |S| ≤ 2 × 10⁵P 中的元素长度均不超过 10

时限 1 秒,内存 128000 KB(125 MB)。

输入输出样例

输入

A AB BA CA BBC
.
ABABACABAABC

输出

11

ABABACABAAB 能分解(A|BA|BA|CA|BA|AB),而再加一个 C 就不行了 ⇒ 答案 11

1★★ 它为什么挂在 KMP 这一章 —— 因为它不用 KMP

★★★ 题单注解自己说了:「不是所有字符串题都要上 KMP」

第 48 章的题单在这道题后面写着:

「字符串 + DP,不用 KMP 也能过。★ 放在这儿是想说明:不是所有字符串题都要上 KMP。」

⇒ 学完一个新工具之后最容易犯的错,就是看什么都像钉子。 这道题里根本没有「在长文本里找一个模式串」这件事 —— 它问的是「能不能拼出来」, 那是一句一维 DP:

   f[0] = true;
   f[k] = 存在元素 e(|e|min(10, k))使 f[k-|e|] 且 S 的第 k-|e| .. k-1 位正好是 e
   答案 = 最大的满足 f[k] 的 k

★ 而这道题真正会咬人的两处,一处在读入,一处在那句「最大的」

p1470.cpp★ 正解:按长度分组 + 哈希,一句 DP
// P1470 最长前缀 —— 正解:一维可达性 DP,和 KMP 一点关系都没有
//
// ★★ 这道题挂在[第 48 章](/ch/48-kmp/)的题单里,题单注解写得很直白:
// 「字符串 + DP,**不用 KMP 也能过**。★ 放在这儿是想说明:不是所有字符串题都要上 KMP」。
// ⇒ 它考的是**读入** + **一句 DP**,而这两样都不需要 next 数组。
//
// ★ DP 一句话:f[k] = 「S 的前 k 个字符能被拼出来吗」
// f[0] = true;f[k] = 存在元素 e(|e| ≤ min(10, k))使 f[k-|e|] 且 S 的第 k-|e|..k-1 位正好是 e。
// 答案 = **最大的**满足 f[k] 的 k。
// ⚠⚠ 注意是「最大的」而不是「第一个断点之前的」—— **f 不是单调的**
// (P = {AA}、S = AAAA 时 f 是 F T F T)⇒ 见 p1470First.cpp。
//
// ⚠ 读入有两处,题面都明写着,而**官方样例一处都没考到**:
// ① 元素集合「**数据可能不止一行**」,以只含一个 `.` 的行结束;
// ② S「用**一行或者多行**的字符串来表示,**换行符并不是序列 S 的一部分**」。
// ⇒ 一路 `cin >> tok` 读到底最省事:它天然跨行(见 p1470Line.cpp —— 那一版假设 S 只有一行)。
#include <bits/stdc++.h>
using namespace std;
int main() {
vector<unordered_set<string>> byLen(11); // 按长度分组:元素长度 ≤ 10
string tok;
while (cin >> tok && tok != ".") if (tok.size() <= 10) byLen[tok.size()].insert(tok);
string s;
while (cin >> tok) s += tok; // ★ 一路读到 EOF,自动跨行
int n = (int)s.size();
vector<char> f((size_t)n + 1, 0);
f[0] = 1;
int ans = 0;
for (int k = 1; k <= n; k++) {
for (int len = 1; len <= 10 && len <= k; len++) {
if (!f[(size_t)(k - len)]) continue;
if (byLen[(size_t)len].count(s.substr((size_t)(k - len), (size_t)len))) { f[(size_t)k] = 1; break; }
}
if (f[(size_t)k]) ans = k; // ⚠ 一路记最大的,不能遇到 false 就停
}
printf("%d\n", ans);
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

2⚠ 第一关:读入 —— 而官方样例正好一处都考不到

★★ 题面把两件事都写明了,可样例里一件都没发生
题面写着 官方样例
元素集合「数据可能不止一行」,以只含 . 的行结束 只有一行
S「用一行或者多行的字符串来表示」,换行不算 S 的一部分 只有一行

⇒ 于是「以为 S 只有一行」那个错法在官方样例上一个字都不会错(照样输出 11)。

★ 而躲开它只要一句话:一路 cin >> tok 读到 EOF —— >> 天然跳过所有空白(包括换行), 两段都不用管有几行。 (⚠ 第 47 章 P1308 那道题正相反:那道题必须 getline, 因为它的「文章」里有空格、而且空格算位置。⇒ 「要不要按行读」是每道题各自的事。

p1470Line.cpp✗ 错法一:以为 S 只有一行
// P1470 ✗ 错法一:以为 S 只有一行
//
// ⚠ 题面写得很清楚:S「用**一行或者多行**的字符串来表示,每行不超过 76 个字符。
// 换行符并不是序列 S 的一部分」。
// ⇒ 只 `getline` 一行(或者只 `cin >> s` 一次),S 就被截断在第一行末尾。
// ★ 触发条件:**S 真的跨了多行**。
// ⚠⚠ 而官方那组样例的 S **正好只有一行** ⇒ **它放过这个错法**。
#include <bits/stdc++.h>
using namespace std;
int main() {
vector<unordered_set<string>> byLen(11);
string tok;
while (cin >> tok && tok != ".") if (tok.size() <= 10) byLen[tok.size()].insert(tok);
string s;
cin >> s; // ⚠ 只读了一段,后面几行全丢了
int n = (int)s.size();
vector<char> f((size_t)n + 1, 0);
f[0] = 1;
int ans = 0;
for (int k = 1; k <= n; k++) {
for (int len = 1; len <= 10 && len <= k; len++) {
if (!f[(size_t)(k - len)]) continue;
if (byLen[(size_t)len].count(s.substr((size_t)(k - len), (size_t)len))) { f[(size_t)k] = 1; break; }
}
if (f[(size_t)k]) ans = k;
}
printf("%d\n", ans);
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

3★★★ 第二关:f 不是单调的 —— 「第一个断点之前」不等于「最大的 k」

★★ 最小反例只要一个元素
   P = {AA}      S = AAAA
   f:  f[0]=T   f[1]=F   f[2]=T   f[3]=F   f[4]=T

⇒ 答案是 4,而「遇到第一个 false 就停」的写法在 k = 1 就退出,输出 0

★ 触发条件:f 中间有「洞」,而答案落在洞的后面。 ⚠ 而这个错法官方样例挡得住(11 → 5)—— 那组样例里 f 恰好也有洞。

⇒ ★ 一句能直接用的判据:只要集合里有长度 ≥ 2 的元素,f 就可能有洞 —— 所以「一路记最大值」和「遇假就停」在这道题上是两个不同的程序。

p1470First.cpp✗ 错法二:遇到第一个断点就停
// P1470 ✗ 错法二:遇到第一个「拼不出来」就停
//
// ⚠ 这一版假设了 **f 是单调的** —— 拼不出前 k 个,就更拼不出前 k+1 个。**那是错的。**
// 最小反例只要一个元素:P = {AA}、S = AAAA ⇒ f 是 **F T F T**,答案是 4,
// 而这一版在 k = 1 就停下来,输出 0。
// ★ 触发条件:**f 中间有「洞」,而答案落在洞的后面**
// —— 也就是「最大的 f[k]」比「第一个断点之前的 k」大。
#include <bits/stdc++.h>
using namespace std;
int main() {
vector<unordered_set<string>> byLen(11);
string tok;
while (cin >> tok && tok != ".") if (tok.size() <= 10) byLen[tok.size()].insert(tok);
string s;
while (cin >> tok) s += tok;
int n = (int)s.size();
vector<char> f((size_t)n + 1, 0);
f[0] = 1;
int ans = 0;
for (int k = 1; k <= n; k++) {
for (int len = 1; len <= 10 && len <= k; len++) {
if (!f[(size_t)(k - len)]) continue;
if (byLen[(size_t)len].count(s.substr((size_t)(k - len), (size_t)len))) { f[(size_t)k] = 1; break; }
}
if (!f[(size_t)k]) break; // ⚠ 以为 f 是单调的
ans = k;
}
printf("%d\n", ans);
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
✗ 错法三:忘了 f[0] = true

「空前缀当然拼得出来(一个元素都不用)」是这句 DP 的起点,漏了它整张表全是 false恒输出 0。 ★ 触发条件:正解的答案不是 0。 ⇒ 和第 23 章 P1164「忘了 f[0]=1」是同一个形状的错,连触发条件都是同一句。

p1470Zero.cpp✗ 错法三:忘了 f[0]

4★★ 「200 个元素挨个试会不会超时」—— 又一次要乘一遍、再量一遍

p1470Count.cpp★ 数「朴素版真做了多少次字符比较」
// P1470 —— 「200 个元素挨个试会不会超时」这笔账。
// ./p1470Count 人话版
// ./p1470Count csv 给 check:viz 用
//
// ★★ 名义上界一句乘法:|S| × card(P) × |e| = 2×10⁵ × 200 × 10 = **4 × 10⁸**。
// ⇒ 「四亿次肯定超时」是句要**乘一遍、再量一遍**才能说的话
// ([第 41 章 P1217](/sol/p1217/)、[第 42 章 P1045](/sol/p1045/)、[第 46 章 P1582](/sol/p1582/) 都中过)。
//
// ⚠ 关键在于那两句短路:`if (len > k || !f[k-len]) continue;` 和「一撞上就 break」
// ⇒ 真正做的比较次数**远到不了 4 亿**,而且**取决于数据形状**:
// · 顺手造的顶格(元素随机、S 由它们拼成)—— 大部分 k 上 `f[k-len]` 是假的,一比都不用比;
// · ★ 卡它的形状(所有元素共享前缀 `AAAAA`、唯一能匹配的那个排在最后、S 全是 A)
// —— 每个 k 都要把 199 个元素比到第 6 个字符才失配。
#include <bits/stdc++.h>
using namespace std;
static const int NS = 200000; // |S| 顶格
/** 朴素版:数「字符比较」做了多少次 */
static long long naiveCmp(const vector<string>& es, const string& s) {
int n = (int)s.size();
vector<char> f((size_t)n + 1, 0);
f[0] = 1;
long long cmp = 0;
for (int k = 1; k <= n; k++) {
for (const string& e : es) {
int len = (int)e.size();
if (len > k || !f[(size_t)(k - len)]) continue;
int t = 0;
while (t < len && s[(size_t)(k - len + t)] == e[(size_t)t]) { cmp++; t++; }
if (t < len) { cmp++; continue; }
f[(size_t)k] = 1;
break;
}
}
return cmp;
}
/** 正解:数「哈希查了多少次」 */
static long long groupLookups(const vector<string>& es, const string& s) {
vector<unordered_set<string>> byLen(11);
for (const string& e : es) byLen[e.size()].insert(e);
int n = (int)s.size();
vector<char> f((size_t)n + 1, 0);
f[0] = 1;
long long look = 0;
for (int k = 1; k <= n; k++)
for (int len = 1; len <= 10 && len <= k; len++) {
if (!f[(size_t)(k - len)]) continue;
look++;
if (byLen[(size_t)len].count(s.substr((size_t)(k - len), (size_t)len))) { f[(size_t)k] = 1; break; }
}
return look;
}
int main(int argc, char** argv) {
bool csv = argc > 1 && string(argv[1]) == "csv";
mt19937 rng(20260908u);
/* ① 顺手造的顶格:200 个随机长度 10 的元素,S 由它们拼起来 */
vector<string> esR;
{
set<string> u;
while ((int)u.size() < 200) {
string e;
for (int i = 0; i < 10; i++) { unsigned c = rng() % 5u; e.push_back((char)('A' + c)); }
u.insert(e);
}
esR.assign(u.begin(), u.end());
}
string sR;
while ((int)sR.size() < NS) { unsigned r = rng() % 200u; sR += esR[r]; }
sR.resize(NS);
/* ② 卡它的形状:共享前缀 AAAAA、唯一能匹配的 "A" 排在最后、S 全是 A */
vector<string> esW;
{
set<string> u;
while ((int)u.size() < 199) {
string e = "AAAAA";
for (int i = 0; i < 5; i++) { unsigned c = rng() % 10u; e.push_back((char)('B' + c)); }
u.insert(e);
}
esW.assign(u.begin(), u.end());
esW.push_back("A");
}
string sW((size_t)NS, 'A');
long long cmpR = naiveCmp(esR, sR), cmpW = naiveCmp(esW, sW);
long long lookR = groupLookups(esR, sR), lookW = groupLookups(esW, sW);
long long bound = (long long)NS * 200 * 10;
if (csv) {
printf("bound,%lld\ncmpRandom,%lld\ncmpWorst,%lld\n", bound, cmpR, cmpW);
printf("lookRandom,%lld\nlookWorst,%lld\n", lookR, lookW);
printf("shapeRatio,%.1f\nvsBound,%.1f\n",
(double)cmpW / (double)cmpR, (double)bound / (double)cmpW);
return 0;
}
printf("名义上界:|S| × card(P) × |e| = %d × 200 × 10 = %lld\n", NS, bound);
printf("★ 朴素版真做的字符比较次数:\n");
printf(" 顺手造的顶格(元素随机 + S 由它们拼成):%12lld\n", cmpR);
printf(" ★ 卡它的形状(共享前缀 + 匹配的排最后):%12lld(差 %.1f 倍)\n",
cmpW, (double)cmpW / (double)cmpR);
printf(" ⇒ 连最坏的那一档也只有名义上界的 1/%.1f —— 两句短路把它砍掉了\n",
(double)bound / (double)cmpW);
printf("★ 正解(按长度分组 + 哈希)查表次数:随机 %lld,卡它的形状 %lld\n", lookR, lookW);
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果
★★★ 名义上界 4 亿,实测最坏 2.39 亿 —— 而它照样过得去

所有人的第一版都是「对每个位置,把 200 个元素挨个试一遍」:

   名义上界 = |S| × card(P) × |e| = 2×10⁵ × 200 × 10 = 4 × 10⁸

⚠ 可那两句短路(if (len > k || !f[k-len]) continue; 和「一撞上就 break」) 把它砍掉了一大截,而砍掉多少取决于数据形状

形状 朴素版的字符比较次数
顺手造的顶格(200 个随机元素、S 由它们拼成) 2 700 909
★ 卡它的形状(元素共享前缀 AAAAA、唯一能匹配的排最后、S 全是 A 238 989 254
倍数 88.5 倍
正解(按长度分组 + 哈希)的查表次数 200 000(两种形状都一样)

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

顺手顶格 ★ 卡它的形状
★ 正解 < 0.01 秒 < 0.01 秒
⚠ 朴素挨个试 0.02 秒 0.10 秒

⇒ ★★★ 「四亿次肯定超时」又一次被打回 —— 连专门卡它的形状也只要 0.10 秒 / 时限 1 秒。 (第 41 章 P1217第 42 章 P1045第 46 章 P1582本章 P3375 都中过同一条。) ⇒ ★★ 而这一页顺带把「顶格 ≠ 最坏」也量了一次: 同样是顶格,只换形状就差 88.5 倍 —— 顺手造那一档,连一成的力气都没使出来。

p1470Naive.cpp★ 另一种正确写法:200 个挨个试(也能过)

5★ 对拍:十二格「触发 ≡ 抓获」,而顺手写的那一档结构上碰不到读入那个坑

p1470Gen.cpp★ 生成器:档 2 起才把 S 拆成多行
// P1470 的生成器:./p1470Gen 种子 [档位]
//
// ★ 三个错法各靠什么现形:
// · p1470Line(以为 S 只有一行)→ **S 真的跨了多行**;
// · p1470First(遇到第一个断点就停)→ **f 中间有洞,而答案落在洞后面**;
// · p1470Zero(忘了 f[0]=true)→ **正解的答案不是 0**。
//
// 档位:
// 0 ★ 顺手写的:元素 5 个(长度 1~3)、S 单行随机 20~40 个字符
// ⚠ 这一档 **S 只有一行** ⇒ 「以为只有一行」那个错法是**结构性的 0**
// —— 而官方那组样例犯的是同一个毛病
// 1 ⚠ 元素长度压到 2~3(★ 长度全 ≥ 2 才容易出「洞」)、S 由元素拼成 + 随机尾巴,仍是单行
// 2 ⚠ 同上,但 **S 拆成 2~4 行**(p1470Line 的专门档)
// 3 ★ 最终档:元素长度固定 2 + 多行 ⇒ 洞最多
//
// ⚠ 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);
const int ALPHA = 3; // A / B / C
auto pick = [&]() { unsigned r = rng() % (unsigned)ALPHA; return (char)('A' + r); };
int loLen = (mode == 0) ? 1 : 2;
int hiLen = (mode == 3) ? 2 : 3;
unsigned rc = rng() % 4u;
int m = 3 + (int)rc; // 3~6 个元素
vector<string> es;
set<string> used;
for (int i = 0; i < m; i++) {
unsigned rl = rng() % (unsigned)(hiLen - loLen + 1);
int len = loLen + (int)rl;
string e;
for (int k = 0; k < len; k++) e.push_back(pick());
if (used.insert(e).second) es.push_back(e);
}
for (size_t i = 0; i < es.size(); i++) printf("%s%c", es[i].c_str(), i + 1 == es.size() ? '\n' : ' ');
printf(".\n");
string s;
if (mode == 0) {
unsigned rn = rng() % 21u;
int n = 20 + (int)rn;
for (int i = 0; i < n; i++) s.push_back(pick());
} else {
unsigned rk = rng() % 8u;
int k = 5 + (int)rk;
for (int i = 0; i < k; i++) { unsigned r = rng() % (unsigned)es.size(); s += es[r]; }
unsigned rt = rng() % 4u; // 随机尾巴:让答案不一定是整串
for (int i = 0; i < (int)rt; i++) s.push_back(pick());
}
if (mode >= 2) { // ⚠ 把 S 拆成 2~4 行
unsigned rl = rng() % 3u;
int lines = 2 + (int)rl;
size_t per = (s.size() + (size_t)lines - 1) / (size_t)lines;
if (per == 0) per = 1;
for (size_t p = 0; p < s.size(); p += per) printf("%s\n", s.substr(p, per).c_str());
} else {
printf("%s\n", s.c_str());
}
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果

四档 × 300 轮(正解 vs 朴素挨个试:1200 轮 0 组不一致):

档位 ✗ 以为 S 一行 ✗ 遇假就停 ✗ 忘了 f[0]
0 ★ 顺手写的(元素长 1~3、S 单行随机) 0 60 158
1 ⚠ 元素长度压到 2~3、S 由元素拼成(仍单行) 0 300 300
2 ⚠ S 拆成 2~4 行 300 300 300
3 ★ 最终档(元素长度固定 2 + 多行) 300 300 300
★★ 两条读得出来的结论
  1. ★★★ 读入那个错法在前两档是结构性的 0 —— 那两档的 S 只有一行, 而这正是官方样例犯的同一个毛病。 ⇒ ★★ 于是这道题上,样例和顺手写的对拍会一起漏掉同一格第 47 章 P1308 那条刚立的规矩,换一道题又成立一次: 样例和对拍一起漏掉的那一格,正是最难自己想到的那一格)。 ⇒ 救法只有一个:照着题面那句「一行或者多行」造一档多行的。

  2. 「元素长度全 ≥ 2」是「洞」的开关 —— 档 0 里元素可以是单个字母, 于是 f 常常一路真到底,「遇假就停」只被抓 60 / 300; 把长度压到 ≥ 2 之后立刻 300 / 300

  3. 十二格「触发 ≡ 抓获」一个不差S 真的跨行 / 最大 k ≠ 第一个断点前的 k / 答案 > 0。

6★ 哪一版就已经能过了

★ 两种写法都能过 —— 而这一页真正的收获是「别上 KMP」
版本 结果 说明
p1470.cpp(分组 + 哈希) AC 查表 20 万次,两种形状都一样
p1470Naive.cpp(200 个挨个试) AC 顺手顶格 0.02 秒、卡它的形状 0.10 秒
✗ 以为 S 一行 WA 官方样例放过,顺手写的对拍前两档也是 0
✗ 遇假就停 WA 样例就死(11 → 5)
✗ 忘了 f[0] WA 恒输出 0

⇒ ★★ 一句话带走:这道题是这一章的「对照组」 —— 它长得很像字符串题(大写字母、前缀、匹配),可它的两个坑 (S 跨多行、f 不单调)和 KMP 一点关系都没有。 ⇒ 学完一个工具之后,第一件该做的事不是找地方用它, 而是先问一句「这道题问的到底是什么」