0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P3375,日期见页头。两边不一致时信原站。
题目描述
给出两个字符串 s₁ 和 s₂,若 s₁ 的区间 [l, r] 子串与 s₂ 完全相同,则称 s₂ 在 s₁ 中出现了,
其出现位置为 l。现在请你求出 s₂ 在 s₁ 中所有出现的位置。
定义一个字符串 s 的 border 为 s 的一个非 s 本身的子串 t,
满足 t 既是 s 的前缀,又是 s 的后缀。
对于 s₂,你还需要求出对于其每个前缀 s' 的最长 border t' 的长度。
输入格式
第一行为一个字符串,即为 s₁。第二行为一个字符串,即为 s₂。
输出格式
首先输出若干行,每行一个整数,按从小到大的顺序输出 s₂ 在 s₁ 中出现的位置。
最后一行输出 |s₂| 个整数,第 i 个整数表示 s₂ 的长度为 i 的前缀的最长 border 长度。
数据规模与约定
本题采用多测试点捆绑测试,共有 4 个子任务。
- Subtask 0(30 分):
|s₁| ≤ 15,|s₂| ≤ 5。 - Subtask 1(40 分):
|s₁| ≤ 10⁴,|s₂| ≤ 10²。 - Subtask 2(30 分):无特殊约定。
- Subtask 3(0 分):Hack。
对于全部的测试点,保证 1 ≤ |s₁|, |s₂| ≤ 10⁶,s₁、s₂ 中均只含大写英文字母。
时限 1 秒,内存 524288 KB(512 MB)。
输入输出样例
输入
ABABABC ABA
输出
1 3 0 0 1
ABA 在 ABABABC 里出现在位置 1 和 3(★ 1 基,而且两次是重叠的)。
最后一行:ABA 的三个前缀 A / AB / ABA 的最长 border 长度分别是 0 / 0 / 1
(ABA 的 A 既是前缀也是后缀)。
样例解释里的那张图(原站的图,本地存了一份):

1★★ 这一页的主线:会咬人的三处全在「约定」和「输出」上,一处都不在 KMP 上
这道题就是第 48 章正文那道题的原题 —— 算法一个字都不用再讲一遍
(next 怎么算、失配时 i 为什么不退、命中后为什么是 j = nxt[j-1],正文第 4~8 步全讲透了)。
★ 而第 48 章的题单在这道题后面挂了一句注解:
「⚠ 它的下标从 1 开始,next 的输出口径也和本书不同 —— 交之前先把两边的约定对一遍」。
这一页就是把那句话兑现成三个错法、一张输出账。 三处差别:
| 本书正文 | 这道题的题面 | |
|---|---|---|
| 出现位置 | 0 基 | ★ 1 基(样例是 1 和 3) |
| 位置怎么输出 | 先打次数,再一行打完 | ★ 一行一个,不打次数 |
| next 那一行 | p 的 next 数组 |
「每个前缀的最长 border」——★ 恰好就是 0 基的 nxt[0..m-1] |
⚠ 而真正会挂人的第四处,题面里一个字都没提:输出量。第 ④ 步整节在算这笔账。
// P3375【模板】KMP —— 正解:KMP + 自己写输出缓冲//// ★★ 这道题就是[第 48 章正文](/ch/48-kmp/)那道题的原题,**可它的约定和本书不一样**,// 三处都要对一遍(题单注解点的就是这件事):// ① 出现位置是 **1 基**(题面:「其出现位置为 l」,样例里 ABA 在 ABABABC 中是 1 和 3)——// 本书正文一律 0 基 ⇒ 照抄要 **+1**(见 p3375Base.cpp);// ② 第二行要的是「s2 的**每个前缀**的最长 border 长度」,一共 |s2| 个数// —— 恰好就是 0 基 nxt[0..m-1],⚠ 但错位一格是最容易犯的(见 p3375Border.cpp);// ③ 位置**按从小到大、一行一个**输出,不输出个数 —— 和正文那种「先打次数再打一行」完全不同。//// ⚠⚠ 而这道题真正的新关卡,正文一次都没量过:**输出**。// 顶格 s1 = 10⁶ 个 A、s2 = 一个 A ⇒ **10⁶ 行位置,约 6.9 MB**(见 p3375Out.cpp)。// ⇒ `cout << x << endl` 会 flush 一百万次,当场 TLE(见 p3375Endl.cpp)。#include <cstdio>#include <cstring>using namespace std;
static const int MAXN = 1000006;static char s1[MAXN], s2[MAXN];static int nxt[MAXN];
static char obuf[1 << 22]; // 4 MiB 输出缓冲:顶格输出约 6.9 MB,分两次冲出去static int opos = 0;static inline void flushOut() { fwrite(obuf, 1, (size_t)opos, stdout); opos = 0; }static inline void putc_(char c) { if (opos == (int)sizeof(obuf)) flushOut(); obuf[opos++] = c; }static inline void writeInt(int x, char tail) { if (opos + 14 > (int)sizeof(obuf)) flushOut(); if (!x) obuf[opos++] = '0'; else { char t[12]; int k = 0; while (x) { t[k++] = (char)('0' + x % 10); x /= 10; } while (k) obuf[opos++] = t[--k]; } putc_(tail);}
int main() { if (scanf("%s%s", s1, s2) != 2) return 0; int n = (int)strlen(s1), m = (int)strlen(s2);
/* nxt[i] = s2[0..i] 的最长 border 长度(0 基)—— 正文第 5 步那五行 */ nxt[0] = 0; for (int i = 1, j = 0; i < m; i++) { while (j > 0 && s2[i] != s2[j]) j = nxt[j - 1]; if (s2[i] == s2[j]) j++; nxt[i] = j; } /* 匹配:i 一步都不退 */ for (int i = 0, j = 0; i < n; i++) { while (j > 0 && s1[i] != s2[j]) j = nxt[j - 1]; if (s1[i] == s2[j]) j++; if (j == m) { writeInt(i - m + 2, '\n'); // ★ +1 变 1 基,再 −m+1 回到起点 ⇒ i-m+2 j = nxt[j - 1]; // ⚠ 不能归零,否则漏掉重叠出现 } } for (int i = 0; i < m; i++) writeInt(nxt[i], i + 1 == m ? '\n' : ' '); flushOut(); return 0;}点「运行 ▶」看结果
题面的子任务分档就是出题人递过来的工具(第 43 章 P1313 那条):
- Subtask 0(30 分):
|s₁| ≤ 15⇒ 随便写; - Subtask 1(40 分):
|s₁| ≤ 10⁴、|s₂| ≤ 10²⇒ 朴素匹配最多 10⁶ 次比较,秒过。
⇒ 上面那份 p3375Brute.cpp 直接交上去就有 70 分,而它一行 KMP 都没有。
2⚠ 错法一:位置照抄本书的 0 基
本书正文(以及第 47 章 P1308)里,位置一律从 0 数;
而这道题的题面写的是「若 s₁ 的区间 [l, r] 子串与 s₂ 完全相同……其出现位置为 l」,
样例给的是 1 和 3。
⇒ 少一个 +1,每一个位置都小 1 —— 而它只在「真的有出现」时才现形:
一次都没出现时两边都只打 border 那一行,逐字节相同(那是能证的 0)。
// P3375 ✗ 错法一:位置照抄本书的 0 基,忘了题面是 1 基//// ⚠ 这是「把本书的写法搬到真题上」最典型的一跤:正文里位置一律从 0 数// (第 47 章 P1308 也是「位置从 0 开始」),而这道题的题面写的是// 「若 s1 的区间 [l, r] 子串与 s2 完全相同……其出现位置为 l」—— 官方样例是 1 和 3。// ★ 触发条件干净得可以写成一句话:**只要 s2 在 s1 里出现过至少一次**。// ⇒ 一次都没出现时它和正解逐字节相同(那一档是能证的 0)。#include <cstdio>#include <cstring>using namespace std;
static const int MAXN = 1000006;static char s1[MAXN], s2[MAXN];static int nxt[MAXN];
int main() { if (scanf("%s%s", s1, s2) != 2) return 0; int n = (int)strlen(s1), m = (int)strlen(s2); nxt[0] = 0; for (int i = 1, j = 0; i < m; i++) { while (j > 0 && s2[i] != s2[j]) j = nxt[j - 1]; if (s2[i] == s2[j]) j++; nxt[i] = j; } for (int i = 0, j = 0; i < n; i++) { while (j > 0 && s1[i] != s2[j]) j = nxt[j - 1]; if (s1[i] == s2[j]) j++; if (j == m) { printf("%d\n", i - m + 1); j = nxt[j - 1]; } // ⚠ 少了那个 +1 } for (int i = 0; i < m; i++) printf("%d%c", nxt[i], i + 1 == m ? '\n' : ' '); return 0;}点「运行 ▶」看结果
3⚠ 错法二:border 那一行错位一格
题面要的是:第 i 个整数 = 长度为 i 的前缀的最长 border 长度(i = 1..m)。
而 0 基的 nxt[i] 说的是「s₂[0..i] 的最长 border」,也就是长度 i+1 的前缀
⇒ 直接输出 nxt[0]、nxt[1]、…、nxt[m-1] 就是对的。
⚠ 可这一步所有人都会犹豫一下,一犹豫就写成「先补一个 0,再输出 nxt[0..m-2]」——整行右移一格。
★ 触发条件:移位之后那一行和原来不同
(⇒ nxt 全是 0 时它反而对 —— 而那正是顺手写的随机数据的常态,见第 ⑤ 步档 0 的 27 / 300)。
4★★★ 第四处题面没提的:输出量 —— 顶格 6.89 MB、10⁶ 行
// P3375 —— 「输出」那一关到底值多少:五种打法,同一份顶格数据各跑一遍。// ./p3375Out <打法> <输出文件> 打法 ∈ endl / nl / nosync / printf / buf// 报告打在标准输出上,**正文那 6.9 MB 写进 <输出文件>**。//// ⚠⚠ 为什么一定要写进**文件**,而不是让它打到管道里(2026-09-07 在 [P1308](/sol/p1308/) 上撞出来的):// 管道那一头是谁、读得多快,**会被一起量进来**。评测机做的是「stdout 重定向到一个文件」,// 所以这里也用 `freopen`,量完再 `dup2` 把 fd 1 换回来打报告。// ⇒ ★★ **量什么都要先问一句「这个数里有没有别人的份」。**//// ★ 顶格形状挑的是「输出最多」的那一种:s1 = 10⁶ 个 A、s2 = 一个 A// ⇒ **每一个位置都命中**,要打 10⁶ 行。#include <bits/stdc++.h>#include <unistd.h>using namespace std;
static const int N = 1000000;static vector<int> pos; // 所有命中位置(1 基)static char obuf[1 << 22];static int opos = 0;static inline void flushOut() { fwrite(obuf, 1, (size_t)opos, stdout); opos = 0; }static inline void writeInt(int x) { if (opos + 14 > (int)sizeof(obuf)) flushOut(); if (!x) obuf[opos++] = '0'; else { char t[12]; int k = 0; while (x) { t[k++] = (char)('0' + x % 10); x /= 10; } while (k) obuf[opos++] = t[--k]; } obuf[opos++] = '\n';}
int main(int argc, char** argv) { string mode = argc > 1 ? argv[1] : "buf"; const char* out = argc > 2 ? argv[2] : "/dev/null";
/* 顶格:s1 全是 A、s2 只有一个 A ⇒ 命中 10⁶ 次(KMP 那部分这里不是重点,直接列出位置)*/ pos.reserve(N); for (int i = 1; i <= N; i++) pos.push_back(i);
int saved = dup(1); if (!freopen(out, "w", stdout)) return 1; auto t0 = chrono::steady_clock::now(); if (mode == "endl") { for (int p : pos) cout << p << endl; // ⚠ 每行 flush } else if (mode == "nl") { for (int p : pos) cout << p << '\n'; } else if (mode == "nosync") { ios::sync_with_stdio(false); cout.tie(nullptr); for (int p : pos) cout << p << '\n'; } else if (mode == "printf") { for (int p : pos) printf("%d\n", p); } else { for (int p : pos) writeInt(p); flushOut(); } cout.flush(); fflush(stdout); auto t1 = chrono::steady_clock::now(); double ms = chrono::duration<double, milli>(t1 - t0).count(); long long bytes = ftell(stdout); dup2(saved, 1); close(saved); clearerr(stdout);
printf("%s,%.1f,%lld\n", mode.c_str(), ms, bytes); return 0;}点「运行 ▶」看结果
输出量最大的形状不是「串最长」,是「命中最多」:
s1 = 10⁶ 个 A s2 = 一个 A
⇒ 每一个位置都命中 ⇒ 要打 10⁶ 行位置实测输出 6 888 896 字节(6.57 MiB) —— 而题面对输出量只字未提。
五种打法(A 机:WSL2 6.18 / 8 核 / 8 GB,2026-09-08,独占;同一份 10⁶ 行,各跑 3 次取中位数):
| 打法 | 耗时 | 相对最快 |
|---|---|---|
cout << x << endl |
247 ms | 28 倍 |
cout << x << '\n' |
28 ms | 3.2 倍 |
关同步 + '\n' |
25 ms | 2.8 倍 |
printf("%d\n", x) |
30 ms | 3.4 倍 |
★ 自己写 4 MiB 缓冲 + fwrite |
★ 8.8 ms | 1.0 |
★★ endl → '\n' 值 8.8 倍 —— 和第 41 章 P3383 量到的一模一样(那页也是 8.8 倍)。
⇒ endl 的问题从来不是换行,是每一行都 flush 一次,这里是一百万次。
★ 而关同步在输出这一侧几乎不值钱(28 → 25 ms,1.1 倍)——
⚠ 对照读入那一侧的 6.2 倍(P3383):并排放着的两条建议,分量差一个数量级。
端到端跑一遍顶格(读 1 MB + KMP + 写 6.89 MB):
| 版本 | 顶格耗时 | 时限 1 秒 |
|---|---|---|
| ★ 正解 | 0.01 秒 | 余量 100 倍 |
✗ endl 版 |
0.25 秒 | ⚠ 它能过,余量只剩 4 倍 |
⇒ ★★★ 「肯定超时」又一次被打回(第 41 章、第 42 章、 第 46 章 都中过)—— 正确的说法是:它不是过不了,是把 100 倍的余量吃到只剩 4 倍。 ⚠ 而这 4 倍是在本机量的,评测机更慢、输出还可能过管道 —— 赌不起。
★★ 顺带一条对拍纪律:endl 版和正解的输出逐字节相同,
四档 1200 轮一次都没被抓到(下一步那张表的最后一列全是 0)。
⇒ 第 48 章第 9 步那条在这儿换了个主语又成立一次:
只影响速度、不影响答案的写法,对拍原理上看不见 —— 这一次它长在输出上。
5★ 对拍:八格「触发 ≡ 抓获」,而顺手写的那一档在验零
// P3375 的生成器:./p3375Gen 种子 [档位]//// ★ 三个待测版本各靠什么现形:// · p3375Base(位置忘了 +1) → **s2 在 s1 里至少出现一次**;// · p3375Border(border 行右移一格)→ **移位之后那一行和原来不同**;// · p3375Endl(用 endl 打) → ⚠ **答案永远对,对拍原理上抓不到**(只能量时间)。//// 档位:// 0 ★ 顺手写的:26 个大写字母随机,|s1| = 20~40、|s2| = 2~5// ⚠ 这一档多半**一次都不出现**(随机 26 个字母里撞上一个长度 3 的串,概率约 1/17576)// 1 ⚠ 保证出现:先摇 s2,再把它塞进 s1 的随机位置(1~3 处)// 2 ⚠ 字母表压到 2 个(A / B)⇒ border 非零、重叠出现满地都是// 3 ★ 最终档:小字母表 + 周期串(s2 = 一小段重复若干次)//// ⚠ 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 >= 2) ? 2 : 26; auto pick = [&]() { unsigned r = rng() % (unsigned)alpha; return (char)('A' + r); };
unsigned r1 = rng() % 21u; int n = 20 + (int)r1; unsigned r2 = rng() % 4u; int m = 2 + (int)r2;
string b; if (mode == 3) { // 周期串:一小段重复若干次 unsigned rp = rng() % 3u; int p = 1 + (int)rp; unsigned rk = rng() % 3u; int k = 2 + (int)rk; string unit; for (int i = 0; i < p; i++) unit.push_back(pick()); for (int i = 0; i < k; i++) b += unit; m = (int)b.size(); } else { for (int i = 0; i < m; i++) b.push_back(pick()); }
string a; for (int i = 0; i < n; i++) a.push_back(pick()); if (mode >= 1) { // 保证出现:塞 1~3 处 unsigned rc = rng() % 3u; int times = 1 + (int)rc; for (int t = 0; t < times && m <= n; t++) { unsigned rp = rng() % (unsigned)(n - m + 1); for (int k = 0; k < m; k++) a[(size_t)rp + (size_t)k] = b[(size_t)k]; } } printf("%s\n%s\n", a.c_str(), b.c_str()); return 0;}点「运行 ▶」看结果
四档 × 300 轮(正解 vs 朴素参照物:1200 轮 0 组不一致):
| 档位 | 有出现的轮数 | ✗ 位置 0 基 | ✗ border 右移 | ✗ endl |
|---|---|---|---|---|
| 0 ★ 顺手写的(26 个大写字母随机) | ⚠ 8 | 8 | 27 | ★ 0 |
| 1 ⚠ 保证 s₂ 出现(塞进 s₁ 里) | 300 | 300 | 27 | ★ 0 |
| 2 ⚠ 字母表压到 2 个 | 300 | 300 | 247 | ★ 0 |
| 3 ★ 最终档(小字母表 + 周期串) | 300 | 300 | 300 | ★ 0 |
-
★★★ 顺手写的那一档在验零 —— 26 个大写字母里随机造一个长度 2~5 的
s₂, 300 轮里只有 8 轮它真的出现在s₁里(一个长度 3 的串撞上的概率约 1/17576)。 ⇒ 另外 292 轮所有版本一起只打 border 那一行,对拍记「通过」。 ⇒ 第 47 章 P1071 那条刚立的规矩,换一道题又成立一次: 先算一句「正解会不会答出东西来」,低了就反着造。 -
★★ 两个「约定」错法的触发条件方向相反 —— 「位置 0 基」要的是有出现 (小字母表一抓一个准),「border 右移」要的是
nxt不全为 0 (随机 26 个字母时nxt几乎全是 0 ⇒ 只有 27 / 300)。 ⇒ ★ 一个旋钮(字母表大小)同时把两列往上推,而它们原来卡在不同的地方。 -
★ 八格「触发 ≡ 抓获」一个不差(前两列),而第三列是结构性的全 0:
endl版的答案永远对。
6★ 官方那唯一一组样例:两个「约定」错法全打死
| ✗ 位置 0 基 | ✗ border 右移 | ✗ endl |
|
|---|---|---|---|
官方样例 ABABABC / ABA |
★ 死(打出 0 和 2) | ★ 死(打出 0 0 0) |
放过(答案相同) |
★ 本书连着量了十几道题的那条规律 —— 官方样例是个「一测就死」的过滤器:它挡住「每一组都错」的,放过「偶尔才错」的 —— 在这一页是正面成立的:两个约定错法每一组都错,所以一交样例就死。
⚠ 而它对第三个错法(endl)完全无能为力,因为那个错法的答案是对的
(第 20 章 P5019 那条:样例这个过滤器筛的是「答案错」)。
⚠⚠ 外加一条只有把两页放在一起才看得见的:
这道题的官方样例输出,最后一行末尾是带一个空格的(0 0 1 )——
也就是说它的评测机忽略行末空白。
而第 47 章 P1598 的题面把「任何一行末尾不要打印多余空格」写了两遍。
⇒ ★★ 「行末空格要不要紧」不是一个能背的经验,是每道题各自的规定 ——
⚠ 但我们自己的对拍仍然要逐字节比(P1598 那页量过:一 strip,一整类 bug 当场消失)。
7★ 哪一版就已经能过了
| 版本 | 结果 | 说明 |
|---|---|---|
★ p3375.cpp |
AC | 顶格 0.01 秒 / 时限 1 秒,余量 100 倍 |
★ p3375Brute.cpp |
⚠ 70 分 | Subtask 0 + 1(10⁴ × 10² = 10⁶ 次比较) |
| ✗ 位置 0 基 | WA | 样例就死 |
| ✗ border 右移 | WA | 样例就死 |
✗ endl 版 |
⚠ 能过(0.25 秒) | 余量从 100 倍掉到 4 倍 —— 赌不起 |
⇒ ★★ 一句话带走:模板题的模板部分是最不容易错的那部分。 这一页的三个坑(1 基 / border 口径 / 输出量), 一个来自「把别处的写法搬过来」,一个来自「题面那句话读了两遍还是没读准」, 一个来自「题面根本没提」—— ⇒ 交模板题之前,把输入输出格式那两段逐字读一遍, 再乘一句「最多要打多少字节」。