阶段 10 · 字符串 · 第 48 章提高组 S

KMP:失配的时候,i 一步都不用退

★ 关键一步是「已经匹配上的那一段自己知道该退到哪」—— 退到它的最长 border,也就是 nxt[j-1]。⚠ 而这一章的转折点在第 3 步:朴素匹配在随机数据上一点都不慢,本机实测甚至比 KMP 还少比一次,最坏那一档才差 496 倍。

需要先学:第 47 章 字符串基础:读进来、切开、比对例题:找出模式串在文本串里所有出现的位置,并输出它的 next 数组建议用时:130 分钟

1一句话问题

给文本串 t 和模式串 p(都只含小写字母,|t| ≤ 10⁶|p| ≤ 10⁵)。

第一行输出 pt 里出现了几次,然后是所有出现的起始下标(0 基,空格隔开,允许重叠); ★ 第二行输出 pnext 数组

输入

ababcabab
abab

输出

2 0 5
0 0 1 2

ababababcabab 里出现在下标 0 和 5。

第二行 0 0 1 2abab 的 next 数组 —— 它是什么、怎么算,第 5 步开始讲。

输入

aaaaa
aa

输出

4 0 1 2 3
0 1

重叠aaaaaaa 里出现在 0、1、2、3 —— 四次,它们互相重叠。

⚠ 这一组是专门放在这儿的:很多写法会漏掉重叠的那几次(本章 wrongNoJump.cpp 就是), 而随机数据一辈子撞不到这种情况

输入

abcdef
xy

输出

0
0 0

一次都没有:第一行只有一个 0(后面没有位置),第二行照样输出 next 数组。

★ 为什么题面要多问一行 next 数组

第 35 章那条:题面多问一句,对拍就多一条腿。

这道题只问位置的话,「next 算错了但答案碰巧对」的写法能一路蒙过去 —— 而 next 恰恰是这一章唯一的新东西。多输出这一行,那一类 bug 就无处可藏。

2朴素匹配:每个位置都对一遍

brute.cppO(nm):每个位置都从头比一遍
// 朴素匹配:把模式串挨个位置对一遍 —— O(nm)
//
// 它是这道题最直白的翻译:文本串的每一个位置都试一次,从那儿开始逐字符比。
// ★ 而这一章第 4 步会证明一件反直觉的事:**在随机数据上它一点都不慢**
// (每个位置平均比不到 1.1 次就失配了)。想看到它的真面目,得自己造数据。
//
// 它同时是对拍的标准答案:思路和 KMP 完全不同 —— 它对「已经匹配过的那一段」
// 没有任何记忆,失配了就把 i 退回去从头再来。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
string t, p;
cin >> t >> p;
int n = (int)t.size(), m = (int)p.size();
vector<int> pos;
for (int i = 0; i + m <= n; i++) {
int j = 0;
while (j < m && t[i + j] == p[j]) j++;
if (j == m) pos.push_back(i);
}
cout << pos.size();
for (int x : pos) cout << ' ' << x;
cout << '\n';
// 第二行:p 的 next 数组。⚠ 这一份用**定义**直接算(枚举所有可能的长度),
// 和 fast.cpp 那个 O(m) 的递推是两条完全不同的路 —— 于是它们能互相对拍。
for (int i = 0; i < m; i++) {
int best = 0;
for (int len = i; len >= 1; len--) { // 从长到短试
if (p.compare(0, len, p, i + 1 - len, len) == 0) { best = len; break; }
}
cout << best << (i + 1 == m ? '\n' : ' ');
}
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

它对「已经比过的那一段」没有任何记忆:只要有一位对不上,就把 i 退回去,从下一个位置重来。 理论上界是 n × m —— 10⁶ × 10⁵ = 10¹¹,按第 45 章那张表,那是几十分钟

3⚠⚠ 转折点:它在随机数据上一点都不慢

按惯例,这一步该实测「暴力有多慢」。可这一章量不出来。

count.cpp换一把尺子:数「字符比较」做了多少次
秒表在小数据上全是 0.00 秒,而比较次数是能数出来的、换台机器也不变的东西。⚠ 它还多数了一个:KMP 里 j 一共回退了多少次。
// 换一把尺子:数「字符比较」做了多少次
//
// 这一章的秒表在小数据上完全没用(都是 0.00 秒),而**比较次数**是能数出来的、
// 换台机器也不变的东西。两种做法各放一个计数器,数的是同一个动作:
// **拿文本里的一个字符和模式里的一个字符比一次。**
//
// ⚠ 还多数了一个:KMP 里 j 一共**回退**了多少次 —— 这是理解「为什么是 O(n+m)」的关键,
// 因为 j 每次 +1 最多 n 次,而每次回退至少让 j 减一,所以回退总数也不超过 n。
//
// 用法:./count < 输入 (和主程序一样的输入:第一行文本,第二行模式)
// ./count csv < 输入 只打 `键,值`,给 check:viz 用
#include <bits/stdc++.h>
using namespace std;
int main(int argc, char** argv) {
ios::sync_with_stdio(false);
cin.tie(nullptr);
bool csv = (argc > 1 && string(argv[1]) == "csv");
string t, p;
cin >> t >> p;
int n = (int)t.size(), m = (int)p.size();
/* ① 朴素匹配 */
long long cmpBrute = 0;
long long hitBrute = 0;
for (int i = 0; i + m <= n; i++) {
int j = 0;
while (j < m) { cmpBrute++; if (t[i + j] != p[j]) break; j++; }
if (j == m) hitBrute++;
}
/* ② KMP(含算 next 的那一趟) */
long long cmpKmp = 0, back = 0;
vector<int> nxt(m, 0);
for (int i = 1; i < m; i++) {
int j = nxt[i - 1];
while (j > 0) { cmpKmp++; if (p[i] == p[j]) break; j = nxt[j - 1]; back++; }
if (j == 0) { cmpKmp++; }
if (p[i] == p[j]) j++;
nxt[i] = j;
}
long long hitKmp = 0;
{
int j = 0;
for (int i = 0; i < n; i++) {
while (j > 0) { cmpKmp++; if (t[i] == p[j]) break; j = nxt[j - 1]; back++; }
if (j == 0) { cmpKmp++; }
if (t[i] == p[j]) j++;
if (j == m) { hitKmp++; j = nxt[j - 1]; back++; }
}
}
if (csv) {
printf("n,%d\nm,%d\nbrute,%lld\nkmp,%lld\nback,%lld\nhit,%lld\nsame,%d\n",
n, m, cmpBrute, cmpKmp, back, hitBrute, hitBrute == hitKmp ? 1 : 0);
return 0;
}
printf("文本 %d 个字符,模式 %d 个字符,出现 %lld 次\n\n", n, m, hitBrute);
printf(" 朴素匹配 比较 %12lld 次 (理论上界 n*m = %lld)\n", cmpBrute, (long long)n * m);
printf(" KMP 比较 %12lld 次 (其中 j 回退了 %lld 次)\n", cmpKmp, back);
printf("\n ★ 朴素 / KMP = %.1f 倍\n", cmpKmp ? (double)cmpBrute / cmpKmp : 0.0);
printf(" ⚠ 两种做法找到的出现次数%s\n", hitBrute == hitKmp ? "一样(这一行是防作弊的)" : "居然不一样,有 bug");
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

本机实测(A 机:WSL2 6.18 / 8 核 / 8 GB,2026-08-25,独占;n = 10⁶m = 1000, 数据来自 ./genBig 1000000 1000 <形状>):

数据形状 朴素比较次数 KMP 比较次数 朴素 / KMP
随机 26 个字母 1 039 887 1 039 354 1.0 倍
文本全 a,模式 aaa…ab 999 001 000 2 000 998 499 倍
文本全 a,模式全 a 999 001 000 1 000 999 998 倍
⚠⚠ 第一行才是这一章真正的起点

随机数据上,朴素匹配比较了 1 039 887 次,KMP 比较了 1 039 354 次 —— 朴素还少比了 533 次(因为 KMP 得多花一趟去算 next)。

「所以要学 KMP」这句话,在你顺手造的数据上是站不住的。

道理其实简单:随机文本里,从任意一个位置开始,第一个字符就对不上的概率是 25/26 —— 平均比 1.04 次就失配了,所以朴素匹配实际上是 O(n)O(nm) 那个上界存在,但随机数据碰不到它

★ 这是第 33 章那条「上界证出来了不等于随机数据碰得到」在字符串上的现场, 而且这一章的倍数是全书最夸张的一次(1 倍 vs 998 倍)。

★ 那就自己造那个最坏情况

要让朴素匹配现出原形,得让它每次都比到最后一格才失配

   文本:  a a a a a a a a a a ... a
   模式:  a a a ... a b
          |<-- m-1 个 a -->|

每个起始位置都要比完 m-1a 才在最后一位撞上 b —— 一次都不能提前退出。 ⇒ 这就是上面那张表的第二行:999 001 000 次,正好是 (n-m+1) × m 的 99.9%。

⚠ 而这种数据你必须自己写生成器造./genBig 1000000 1000 worst), 它不会从天上掉下来 —— 第 12 步整节都在讲这件事。

秒表这时候才有话说(同一台机器、同样独占):

数据 n m 朴素 KMP 倍数
随机 10⁶ 1 000 0.004 秒 0.003 秒
随机 10⁶ 10 000 0.091 秒 0.003 秒 27×
★ 最坏 10⁶ 1 000 0.239 秒 0.004 秒 63×
★ 最坏 10⁶ 10 000 2.267 秒 0.005 秒 502×
同题对比:⚠ 朴素 O(nm) vs ✓ KMP O(n+m)
⚠ 这个生成器默认造的是「最坏」形状(文本全 a、模式 aaa…ab)。先跑 200000 看一眼,再改成 2000000 —— 左边会明显卡住,右边纹丝不动。
⚠ 朴素 O(nm)
✓ KMP O(n+m)

4★ 关键的一步:失配时,i 一步都不用退

看朴素匹配失配的那一刻,它扔掉了一件已经知道的事

   t:  a b a b c ...
   p:  a b a b
                ^ 这里失配(t 的 c 对不上 p 的第 5 位)

   朴素的做法:i 退回去,从 t[1] 重新开始对
   ⇒ 可我们**已经知道** t[0..3] = "abab" 了 —— 从 t[1] 开始对,等于把这件事忘掉重来
★★ 那一段自己知道该退到哪

已经匹配上的是 p[0..j-1](也就是 t 上同样的一段)。现在要问的是:

把模式串往右滑,最少滑多少格,才可能重新对上?

滑到某个位置能对上,等价于:p 的某个前缀 == 已匹配那段的某个后缀。 ⇒ 要滑得最少,就取最长的那个 —— 它有个名字:

border:一个串里,最长的、既是它的真前缀又是它的真后缀的那一段。 (真 = 不能是整个串自己。)

比如 abab 的 border 是 ab(长 2);aabaa 的 border 是 aa(长 2);abcde 没有 border(长 0)。

⇒ 于是失配时只要一句:

   j = nxt[j - 1]        // 退到「已匹配那段」的最长 border 长度

i 一动不动 —— 因为退过去之后,前面那 nxt[j-1] 个字符是已经验过的(后缀 = 前缀)。

⚠ 全书的 next 约定,先看清这一条再去对别人的模板
   nxt[i] = 「p[0..i] 这个前缀里,最长的 border 有多长」

· 长度语义(不是「指针该退到哪」)· 0 基下标 · nxt[0] 恒为 0。

⚠ 网上的模板两派都有:另一派把 next 整个往右挪一位,或者拿 -1 当哨兵。 两派的代码都对,但混着抄必然错。⇒ 拿别处的模板对照时,先看它的 next[0] 是 0 还是 −1。

5next 怎么算:让 p 和它自己匹配

★★ 算 next 的循环,本身就是一次匹配

要算 nxt[i],就是问「p[0..i] 的最长 border 多长」。而 border 的定义里, 前缀和后缀都是 p 自己的一段 —— 所以这件事就是p 去匹配 p

于是那个循环和匹配的循环长得一模一样:

   j = nxt[i-1]                        // 上一位的答案,就是这一位的候选
   while (j > 0 && p[i] != p[j])       // 接不上就退到更短的 border
       j = nxt[j-1]
   if (p[i] == p[j]) j++
   nxt[i] = j

⚠ 那句 j = nxt[j-1] 和匹配时是同一句 —— 整个 KMP 只有这一个动作,用了两遍。

6动画一:看 next 是怎么退出来的

★ 换到「aabaaab(要连退两次)」那一组,盯住红色的那一步:j 从 3 直接掉到 1, 中间那两格是跳过去的 —— 因为「退而求其次」不是退一格,而是退到那一段自己的 border。 (顺手写成 j-- 就是本章 wrongNextBack.cpp。)

算 next:接不上的时候,退到「那一段自己的最长 border」
第 1 / 13 步
下标
0
1
2
3
4
p
a
b
a
b
a
nxt
0
■ 正在算的这一位 p[0] ■ 拿来比的 p[0] ■ 当前候选的那段 border(长 0)
nxt[0] = 0 —— 只有一个字符的前缀,真前缀和真后缀都是空的,长度只能是 0。

7正解

fast.cpp正解:KMP,O(n + m)
// 正解:KMP,O(n + m)
//
// ★★ 全书的 next 约定(第三节写着,别换):
// nxt[i] = 「p[0..i] 这个前缀里,最长的、既是真前缀又是真后缀的那一段有多长」
// · 长度语义(不是「指针该退到哪」那一派)· 0 基下标 · nxt[0] 恒为 0。
// ⚠ 网上的模板两派都有(另一派把 next 往右挪一位、或者拿 −1 当哨兵),混着抄必然错。
//
// 关键的一步只有一句话:**失配时 i 一步都不退**。
// 已经匹配上的那一段 p[0..j-1] 是知道自己该退到哪的 —— 退到它的最长 border,
// 也就是 j = nxt[j-1]。因为那一段的后缀 = 前缀,所以「前面那截已经对上了」这件事不用重新验。
//
// ⚠ 算 nxt 的那个循环,本身就是「p 和自己匹配」—— 同一套回退逻辑用了两遍。
// 这一句说不透,后面就只能背代码了。
#include <bits/stdc++.h>
using namespace std;
/** nxt[i] = p[0..i] 的最长 border 长度 */
vector<int> buildNext(const string& p) {
int m = (int)p.size();
vector<int> nxt(m, 0);
for (int i = 1; i < m; i++) {
int j = nxt[i - 1]; // 上一位的 border 长度,也是「下一个要比的位置」
while (j > 0 && p[i] != p[j]) j = nxt[j - 1]; // 对不上就退到更短的 border
if (p[i] == p[j]) j++;
nxt[i] = j;
}
return nxt;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
string t, p;
cin >> t >> p;
int n = (int)t.size(), m = (int)p.size();
vector<int> nxt = buildNext(p);
vector<int> pos;
int j = 0; // 当前已经匹配上的长度
for (int i = 0; i < n; i++) { // ★ i 只往前走,一步都不退
while (j > 0 && t[i] != p[j]) j = nxt[j - 1];
if (t[i] == p[j]) j++;
if (j == m) {
pos.push_back(i - m + 1);
j = nxt[j - 1]; // ⚠ 找到一个之后要继续,允许**重叠**的下一次出现
}
}
cout << pos.size();
for (int x : pos) cout << ' ' << x;
cout << '\n';
for (int i = 0; i < m; i++) cout << nxt[i] << (i + 1 == m ? '\n' : ' ');
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★ 为什么它是 O(n + m):盯住 j

j 每轮最多 +1(对上一次加一),所以整个过程里 j 一共增加不超过 n 次。 而每次回退至少让 j 减一,且 j 从不为负 ⇒ 回退的总次数也不超过 n

⇒ 内层那个 while 看着吓人,但它一辈子转不了几次。 本机实测(n = 10⁶、最坏数据):回退一共 999 999 次,和 n 同一个量级 —— 正好对上这个证明。 (count.cpp 把这个数也打出来了。)

8动画二:匹配时 i 一步都不退

盯住上面那行的 i:它从头走到尾,一次都没有往回。往右滑的是下面那行模式串。

★ 换到「重叠出现:aaaaa 里找 aa」那一组 —— 命中之后 j 不是归零,而是退到 nxt[j-1], 所以紧接着的那一次重叠出现不会被漏掉。

匹配:模式串往右滑,而文本上的 i 一步都不退
第 1 / 14 步
t
a
b
a
b
c
a
b
a
b
p
a
b
a
b
i = 0
j(已匹配长度)= 0
比较了 0
找到 0
从头开始。j 是「已经匹配上的长度」,★ 而 i 从这里到结束一步都不会退。

9★ 对拍:四个写错的版本

标准答案是 brute.cpp —— 它和 KMP 的思路完全不同(没有任何记忆,失配就整段重来), 而且它的 next 数组是按定义枚举算的(从长到短试每个长度),和 fast.cpp 那个 O(m) 递推是两条路。

对拍器
★ 生成器不给档位时跑的就是最终档(第 10 步那张表里的档位 4):小字母表 + 周期模式串,专门造 border 和重叠。
// 正解:KMP,O(n + m)
//
// ★★ 全书的 next 约定(第三节写着,别换):
// nxt[i] = 「p[0..i] 这个前缀里,最长的、既是真前缀又是真后缀的那一段有多长」
// · 长度语义(不是「指针该退到哪」那一派)· 0 基下标 · nxt[0] 恒为 0。
// ⚠ 网上的模板两派都有(另一派把 next 往右挪一位、或者拿 −1 当哨兵),混着抄必然错。
//
// 关键的一步只有一句话:**失配时 i 一步都不退**。
// 已经匹配上的那一段 p[0..j-1] 是知道自己该退到哪的 —— 退到它的最长 border,
// 也就是 j = nxt[j-1]。因为那一段的后缀 = 前缀,所以「前面那截已经对上了」这件事不用重新验。
//
// ⚠ 算 nxt 的那个循环,本身就是「p 和自己匹配」—— 同一套回退逻辑用了两遍。
// 这一句说不透,后面就只能背代码了。
#include <bits/stdc++.h>
using namespace std;
/** nxt[i] = p[0..i] 的最长 border 长度 */
vector<int> buildNext(const string& p) {
int m = (int)p.size();
vector<int> nxt(m, 0);
for (int i = 1; i < m; i++) {
int j = nxt[i - 1]; // 上一位的 border 长度,也是「下一个要比的位置」
while (j > 0 && p[i] != p[j]) j = nxt[j - 1]; // 对不上就退到更短的 border
if (p[i] == p[j]) j++;
nxt[i] = j;
}
return nxt;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
string t, p;
cin >> t >> p;
int n = (int)t.size(), m = (int)p.size();
vector<int> nxt = buildNext(p);
vector<int> pos;
int j = 0; // 当前已经匹配上的长度
for (int i = 0; i < n; i++) { // ★ i 只往前走,一步都不退
while (j > 0 && t[i] != p[j]) j = nxt[j - 1];
if (t[i] == p[j]) j++;
if (j == m) {
pos.push_back(i - m + 1);
j = nxt[j - 1]; // ⚠ 找到一个之后要继续,允许**重叠**的下一次出现
}
}
cout << pos.size();
for (int x : pos) cout << ' ' << x;
cout << '\n';
for (int i = 0; i < m; i++) cout << nxt[i] << (i + 1 == m ? '\n' : ' ');
return 0;
}
点一下即可编辑

300 轮实测(种子 1..300,最终档 4):

故意写错的地方 被抓 靠什么现形
wrongNoJump:命中后 j = 0 86 / 300 重叠出现
wrongIfWhile:回退那句 while 写成 if 60 / 300 border 链够长(一次退不到位)
wrongNextBack:算 next 时退成 j-- ★ 32 / 300 border 长度 ≥ 2,而且要真的退
wrongSlow:失配时 i 也退回去 ★★ 0 / 300 对拍原理上抓不到它
⚠⚠ 第 ③ 行:它的答案永远是对的

wrongSlow.cpp 保留了 next 数组、保留了 KMP 的外壳,唯独把「i 不退」那件事丢了 —— 于是它退化成了朴素匹配,只是外表还像 KMP。

⇒ 它的输出和正解逐字节相同,三百轮、三万轮都一样。对拍看不见它。

抓它只有一条路:换尺子,数比较次数。 本机实测(n = 10⁶ 最坏数据):

比较次数
正解 KMP 2 000 998
wrongSlow 999 001 000(和朴素一模一样)

★ 第 36 章那条第四盲区在这一章又出现了: 所有只影响复杂度、不影响答案的写法,对拍原理上全都抓不到。 ⇒ 而这一次它尤其危险 —— 因为那份代码看起来就是 KMP

四个错误版本(点开看)
wrongNoJump.cpp② 命中后 j 归零 → 漏掉重叠
wrongIfWhile.cpp④ while 写成了 if → 只退一次
wrongNextBack.cpp① 算 next 时退成 j--
wrongSlow.cpp③ ★ 答案永远对,只是退化成了 O(nm)

★ 每一份的文件头都写着「它靠什么现形」—— 那不是注释,那是下一步调生成器的依据。

10★★ 生成器:顺手写的那一档,一个 bug 都抓不到

档位 相对上一档拧了什么 ① NextBack ② NoJump ④ IfWhile ③ Slow
0(顺手写法) 26 个字母随机,文本 2040、模式 25 0 0 0 0
1 ★ 字母表压到 2 个(a / b) 5 93 30 0
2 ★ 周期模式(短):13 的块重复 24 遍 4 17 11 0
3 ★ 周期模式(长):14 的块重复 36 遍,改中间一个字符 6 12 6 0
4 ★ 最终档 = 1 + 3 32 86 60 0
5 对照 = 4 − 小字母表 6 12 6 0
6 对照 = 4 − 周期模式 5 93 30 0
★★★ 第一行是这一章最该记住的一行

顺手写的生成器(26 个字母随机),三个真 bug 一个都抓不到 —— 全是 0 / 300。

原因和第 3 步是同一个:随机的 26 字母串几乎处处 border = 0 (相邻两位相同的概率只有 1/26),于是

  • 没有 border ⇒ j 永远退不了 ⇒ ① 和 ④ 那两句退法怎么写都一样;
  • 模式串不会重复出现 ⇒ ② 那个「重叠」根本没机会发生。

KMP 的所有 bug 都活在「有 border」这件事上,而随机数据恰好一点 border 都没有。 这就是为什么这一章的生成器必须先把字母表压小

★★ 两个旋钮不是相加,是相乘 —— 全书第一次

看 ① NextBack 那一列:

抓到
只开「小字母表」(档位 1) 5
只开「周期模式」(档位 3) 6
两个一起开(档位 4) 32

5 + 6 = 11,而实测是 32 —— 两个旋钮互相成就了。

道理说得清:小字母表让 border 存在,周期模式让 border 变长、成链; 而 j--j = nxt[j-1] 只有在「border 长度 ≥ 2 且真的要退」时才分得出来 —— 那需要两个条件同时成立。

⚠ 这和前面几章记了很多次的那条正好相反(第 32、34、35 章:「调优不可加,改动之间会互相吃掉」)。 ⇒ 收紧成一句:改动之间会互相影响,方向两头都有 —— 所以每加一处,都要在最终环境里重新量一次,而不是在它刚加进来的那个环境里。

⚠ 老实账:周期模式对 ② NoJump 反而是负分

② 那一列:只开小字母表是 93,加上周期模式之后掉到 86

因为周期模式会把模式串造得更长(14 的块重复 36 遍,最长 24 个字符), 而模式串一长,它在 20~40 个字符的文本里能出现的次数就变少了 —— 重叠的机会跟着变少。

⇒ 它仍然留着,理由不是抓获率而是另外两列(① 5 → 32、④ 30 → 60)。 这笔账要明写,不许粉饰成「改了就是更好」(第 27 章那条)。

gen.cpp(七个档位)两个旋钮,每一个都写清了它是为哪个 bug 拧的

11这一章没讲的,和下一章

⚠ 边界
没讲的 一句话
字符串哈希 下一章(第 49 章):把子串变成一个数,同样 O(n) 解这道题 —— ★ 以及它为什么会错
next 的另一个用途 n − nxt[n-1] 就是这个串的最小循环节长度 —— 很多题真正考的是这个
AC 自动机 多个模式串一起匹配,是 KMP 和 Trie(第 50 章)的合体,S 组进阶
Z 函数 / 后缀数组 本书不讲;用到时按同样的路子(先暴力、再实测、再关键一步)自己补

★ 顺带一句考场上的实话:多数字符串匹配题用 string::find 就够了 —— 它在实现里做了优化,随机数据上快得很。真正需要 KMP 的,是「最坏数据 + 大 n」那一档, 或者题目要的其实是 next 数组本身(比如问循环节)。

12自测

自测清单0 / 13
配套练习
这一章我卡在哪(过一个月回来看,这几行比整章正文都值钱)
这一章记住三句话
  1. 失配时 i 一步都不用退 —— 已经匹配上的那一段自己知道该退到哪:退到它的最长 border, 也就是 j = nxt[j-1]。而算 next 的那一趟,本身就是「p 和自己匹配」,用的是同一句。
  2. ⚠⚠ 「朴素匹配慢」在随机数据上复现不出来 —— 本机实测它甚至比 KMP 还少比一次。 要看见 O(nm),得自己造「文本全 a、模式 aaa…ab」那种数据。
  3. ★★ KMP 的 bug 全活在「有 border」这件事上,而随机的 26 字母串几乎处处没有 border —— 所以顺手写的生成器,三个真 bug 一个都抓不到(0 / 300)。