CSP-S 提高级课程中心 / 算法进阶 / 第28讲
CSP-S 提高级 · 算法进阶 · s5-06

字符串匹配 之 KMP 算法

指针不需要回头 —— 用一张 next 表把 O(nm) 降到 O(n+m),这是字符串算法里最重要的一次「信息复用」。

O(n+m) 线性匹配 1 张 next 表 3 个完整案例 2 组交互动画 12 道洛谷原题
本讲 · 第 28/52 讲KMP字符串匹配
模块 01

情景导航

用一个「已经对上了好几个字」的场景,让你自己发现:重复比较是可以避免的。

故事验钞机的暗记

银行验钞机上有一个小屏幕,要在一张钞票的整条防伪带上找出暗记序列 「金金银金金」。防伪带上的字符是机器随机印上去的,比如 「银金金金金金银金金」。

初级工人的做法是:拿着暗记从头对齐,一个字一个字地对;一旦发现第 5 个字对不上,就把暗记整体右移一格,从第 1 个字重新开始对。

这个办法在「银金金金金金金金金金金金」这种长串上会崩溃 —— 每次都要从头比 4 个字才发现第 5 个不对,白白浪费了大量时间,总比较次数高达 n × m。

老师傅看了一眼说:「你已经知道前 4 个字是金金金金了,那为什么还要重新看一遍?直接把暗记往后挪到下一个可能的位置就行。」

「已经匹配成功的前缀,本身就蕴含了「还能往哪挪」的全部信息。

把这句直觉写成一张表,就是 next 数组;拿着这张表扫描,主串指针永远不回头,复杂度降到 O(n + m)。这就是 KMP。

💡 KMP 到底省掉了什么

朴素匹配里,失配后我们让主串指针回退,然后重新比较已经比较过的那一段。而这一段之所以能匹配上,说明它和模式串的前缀完全相同 —— 这个信息早就在我们手里了。

KMP 的思路是:主串指针 i 只前进、不回头;失配时只把模式串指针 j 往回跳到「下一个可能匹配的位置」。这个「跳到哪里」由模式串自身决定,与主串无关,所以可以预处理成一张表。

一句话总结:KMP = 主串不回退 + 失配跳 next。

①生活类比
📖

背课文接下一句

背错了不用从头重背,从最近一个「能接上的地方」继续 —— 这就是「最长相等前后缀」的直觉。

→ next 数组
🧵

花纹对齐

布上印花是循环图案,撕坏一段后要从图案的「重复起点」重新对齐,而不是差一格就重来。

→ 循环节 n − next[n]
🎬

视频卡顿续播

播放卡住后用「已缓冲到第 30 秒」这一信息直接从 30 秒接着播,而不是从 0 秒重放。

→ 主串指针不回退
🚶

走错路不用回原点

发现走错时,退到「上一个岔路口」换个方向,而不是退回出发点。

→ j ← next[j]
②核心定义

字符串匹配问题:给定主串 T(长度 n)与模式串 P(长度 m),找出 P 在 T 中所有出现的位置(或判断是否存在)。

前缀 / 后缀:对字符串 S,其前缀是 S[1..k],其后缀是 S[m−k+1..m]。若 k < |S|,则称为真前缀 / 真后缀。

next 数组(失配指针 / 最长相等前后缀):next[i] 表示「模式串长度为 i 的前缀」中,既是真前缀又是真后缀的最长字符串的长度。约定 next[1] = 0。

KMP 匹配规则:主串指针 i 从 1 走到 n;当 T[i] == P[j+1] 时 j 前进;否则 j = next[j](j 已经不能再退时就 i++)。当 j 达到 m 时记录一次出现,并令 j = next[m] 继续找下一次。

最小循环节:字符串 S(长度 n)的最小循环节长度为 n − next[n];若 n % (n − next[n]) == 0,则 S 恰好由该循环节重复 n/(n−next[n]) 次构成。

📌 本讲核心知识地图

板块核心内容大纲
朴素匹配失配后主串回退,最坏 O(nm)【6】字符串匹配
next 数组最长相等真前后缀长度,next[1] = 0【6】KMP算法
求 next模式串自己和自己匹配,双指针 O(m)【6】KMP算法
主匹配i 不回退,失配时 j = next[j]【6】KMP算法
复杂度O(n + m),j 最多增加 n 次故也最多减少 n 次【6】KMP算法
循环节最小周期 = n − next[n],判断是否整除【6】KMP算法
扩展Z 函数(扩展 KMP)、AC 自动机的基础【6】字符串匹配
③核心口诀
🔑 KMP

主串指针不回头,失配就跳 next;
next 是「最长相等前后缀」,自己和自己匹配求出来;
匹配成功记位置,然后 j = next[m] 接着找。

🔑 循环节

周期长度 = n − next[n];
能整除就是完整循环节,不能整除就只是周期;
「n 减去最长相等前后缀」= 最小的重复步长。

模块 02

知识探究

先把朴素做法的浪费看清楚,再一步步造出 next 表,最后用它完成线性匹配。

1朴素匹配:O(nm) 浪费在哪里 对照
C++朴素(暴力)字符串匹配
// 1-indexed,T 主串长度 n,P 模式串长度 m
int naive(char *T, int n, char *P, int m) {
    for (int i = 1; i + m - 1 <= n; i++) {   // 枚举所有可能的起始位置
        int j = 1;
        while (j <= m && T[i + j - 1] == P[j]) j++;   // 一个个往后比
        if (j > m) return i;                    // 全对上了,返回起始位置
    }
    return -1;
}
运行结果与最坏情况
输入: T = "ABAAABAB"   P = "ABAB"
输出: 5        (T[5..8] = "ABAB")

最坏情况:T = "aaaaaaaaaab",P = "aaab"
  每一轮都要比较 m=4 次才发现第 4 个字符不匹配(前面 3 个白比了)
  共约 n 轮 → 总比较次数 ≈ n × m

n = 10⁶, m = 10³ 时:约 10⁹ 次比较 —— 必然超时。

🔍 浪费的本质:重复比较已经确定匹配的部分

上例中,第 1 轮比较 T[1..3] 与 P[1..3] 全部匹配、T[4] 与 P[4] 失配。此时我们已经确定知道 T[1..3] = "aaa"。

第 2 轮时,朴素算法却让主串指针退回 i=2,重新拿 P[1] 和 T[2] 比 —— 而 T[2] 早就知道是 'a' 了!这些比较全部是白做的。

KMP 要做的就是:把「已经匹配的部分」这个信息利用起来,让 i 不回头。

先明确概念,再用一张表把 P = "ABABACA" 的 next 数组手工算出来。

i(前缀长度)前缀 P[1..i]最长的「相等真前后缀」next[i]
1A真前缀集合为空0
2AB前缀 "A" vs 后缀 "B" —— 不等0
3ABA"A" == "A"(长度 1)1
4ABAB"AB" == "AB"(长度 2)2
5ABABA"ABA" == "ABA"(长度 3)3
6ABABAC后缀以 C 结尾,前缀首字母是 A —— 无0
7ABABACA"A" == "A"(长度 1)1
📊 next 数组的完整结果(下标 1..7)
next[1]0
next[2]0
next[3]1
next[4]2
next[5]3
next[6]0
next[7]1
含义示例:next[5] = 3 表示 "ABABA" 的最长相等真前后缀是 "ABA"(长度 3)

💡 next[i] 的物理意义:失配时该退到哪里

假设模式串前 j 个字符已经匹配好,第 j+1 个字符失配了。此时我们希望找「一个更短的前缀,它正好等于刚才匹配部分的某个后缀」,这样就能把模式串整体右移一段、继续匹配。

而这个「更短的前缀」长度,正是 next[j]。所以失配时的操作就是 j = next[j],模式串相对主串向右滑动了 j − next[j] 格。右移的格数自动是「最小可能值」,没有遗漏任何一次可能的匹配。

3双指针求 next:模式串自己和自己匹配 难点

求 next 数组最优雅的理解方式:把模式串 P 同时当作「主串」和「模式串」,让 P 与自己的后缀做匹配。

C++求 next 数组(5 行核心代码)
// next[i] = P[1..i] 的最长相等真前后缀长度;next[1] = 0
void buildNext(char *P, int m, int *nxt) {
    nxt[1] = 0;                          // 长度为 1,没有真前后缀
    int j = 0;                             // j:当前已匹配的「前缀长度」
    for (int i = 2; i <= m; i++) {           // i:正在计算的 next 下标(相当于主串指针)
        while (j > 0 && P[i] != P[j + 1]) j = nxt[j];   // ★ 失配:前缀指针回退
        if (P[i] == P[j + 1]) j++;        // 匹配成功,前缀长度 +1
        nxt[i] = j;                          // 记录答案
    }
}
🔁 分步求 next:P = "ABABACA"(下标 1..7)
0初始化:nxt[1] = 0,j = 0。指针 i 从 2 开始,我们要让 P 的后缀去匹配 P 的前缀。
1i=2:P[2]='B' 与 P[j+1]=P[1]='A' 比较 → 不等,且 j=0 无法再退 → nxt[2] = 0。含义:"AB" 没有相等真前后缀。
2i=3:P[3]='A' 与 P[1]='A' 相等 → j 变成 1 → nxt[3] = 1。含义:"ABA" 的 "A" 前后缀相等。
3i=4:P[4]='B' 与 P[j+1]=P[2]='B' 相等 → j 变成 2 → nxt[4] = 2。含义:"ABAB" 的 "AB" 前后缀相等。
4i=5:P[5]='A' 与 P[3]='A' 相等 → j 变成 3 → nxt[5] = 3。含义:"ABABA" 的 "ABA" 前后缀相等。
5i=6:P[6]='C' 与 P[4]='B' 不等 → 失配,j = nxt[3] = 1;再比较 P[6]='C' 与 P[2]='B' → 仍不等;j = nxt[1] = 0,P[6] 与 P[1]='A' 也不等 → nxt[6] = 0。
6i=7:P[7]='A' 与 P[1]='A' 相等 → j = 1 → nxt[7] = 1。
7结果:nxt = [0, 0, 1, 2, 3, 0, 1](下标 1..7)。整个过程 i 从 2 走到 7 一次,j 只在失配时回退,总工作量 O(m)。
理解要点:j = nxt[j] 这一步是「不可能再有大一点的答案了,退一个小的试试」,与主匹配中的失配跳转完全同构。
4KMP 主匹配过程 核心
C++KMP 主匹配(与求 next 的结构完全对称)
// 在 T[1..n] 中找 P[1..m],输出所有出现位置(起始下标)
void kmp(char *T, int n, char *P, int m, int *nxt) {
    int j = 0;                            // j:模式串已匹配的长度
    for (int i = 1; i <= n; i++) {           // ★ 主串指针 i 只增不减
        while (j > 0 && T[i] != P[j + 1]) j = nxt[j];   // 失配:模式串指针回退
        if (T[i] == P[j + 1]) j++;        // 匹配成功,一起前进
        if (j == m) {                        // 找到了一个完整的匹配
            cout << i - m + 1 << " ";         // 起始位置
            j = nxt[m];                      // ★ 继续找下一次(利用后缀信息)
        }
    }
}
A
B
A
A
A
B
A
B
A
B
A
B
动画说明:第一行是主串 T = ABAAABAB,第二行是模式串 P = ABAB(它的 next = [0, 0, 1, 2])。格子会依次循环高亮,请对照下面的分步表格看指针是怎么走的。
⚡ 匹配全过程:T = "ABAAABAB",P = "ABAB"(nxt = [0,0,1,2])
1i=1:T[1]='A' 与 P[1]='A' 匹配 → j=1。对齐情况:T: ABAAABAB / P: ABAB(P 左端在 T 的第 1 位)。
2i=2、i=3:T[2]='B'=P[2],T[3]='A'=P[3] → j 依次变成 2、3。已经匹配了 "ABA" 三个字符。
3i=4:T[4]='A' 与 P[4]='B' 失配!此时 j=3,执行 j = nxt[3] = 1。模式串整体右移 3−1 = 2 格,P 左端来到 T 的第 3 位。主串指针 i 始终停在 4,没有回头。
4回退后 j=1,用 j+1=2 继续:T[4]='A' 与 P[2]='B' 不等 → j = nxt[1] = 0。j 已经是 0,无法再退,本轮的 i=4 结束。
5i=5:T[5]='A' 与 P[1]='A' 匹配 → j=1。注意这里没有重新扫描 T[1..4],直接拿着 i=5 继续。
6i=6、7、8:T[6]='B'=P[2] → j=2;T[7]='A'=P[3] → j=3;T[8]='B'=P[4] → j=4 = m,匹配成功!输出起始位置 i − m + 1 = 8 − 4 + 1 = 5。
7收尾:令 j = nxt[4] = 2 继续(为了找重叠的匹配)。T 已经扫完,算法结束。整个过程 i 从 1 走到 8 只走了一遍,j 的回退总次数不超过 n。
对照记忆:求 next 的循环和主匹配的循环,除了「主串换成了 P 自己」之外,代码一字不差。
案例案例1:P3375 KMP 模板(完整代码) 必背模板

题目:给定主串 S1 与模式串 S2,求 S2 在 S1 中所有出现的位置(从 1 开始编号),并输出 S2 的 next 数组。

C++P3375 【模板】KMP 字符串匹配 — 完整代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000005;
char T[MAXN], P[MAXN];
int n, m, nxt[MAXN];

// 求 next:P 自己和自己匹配
void buildNext() {
    nxt[1] = 0;
    int j = 0;
    for (int i = 2; i <= m; i++) {
        while (j > 0 && P[i] != P[j + 1]) j = nxt[j];
        if (P[i] == P[j + 1]) j++;
        nxt[i] = j;
    }
}

int main() {
    scanf("%s%s", T + 1, P + 1);   // ★ 从下标 1 开始读入
    n = strlen(T + 1);
    m = strlen(P + 1);

    buildNext();                            // ① 预处理 next,O(m)

    int j = 0;                              // ② 主匹配,O(n)
    for (int i = 1; i <= n; i++) {
        while (j > 0 && T[i] != P[j + 1]) j = nxt[j];
        if (T[i] == P[j + 1]) j++;
        if (j == m) {
            printf("%d\n", i - m + 1);        // 输出出现位置
            j = nxt[m];                        // 继续找重叠匹配
        }
    }

    for (int i = 1; i <= m; i++)        // ③ 输出 next 数组
        printf("%d%c", nxt[i], i == m ? '\n' : ' ');
    return 0;
}
运行结果
输入: ABAAABAB ABAB
输出: 5
      0 0 1 2

输入: ABABACA ABABACA
输出: 1
      0 0 1 2 3 0 1

输入: ABABABAB ABAB
输出: 1
      3
      5
      0 0 1 2

说明:P = "ABAB" 的 next 数组为 [0, 0, 1, 2],
      T 中从第 1、3、5 位开始都能匹配上(允许重叠)。

复杂度:预处理 O(m),匹配 O(n),总计 O(n + m)。
        n = m = 10⁶ 时约 2×10⁶ 次操作,0.05 秒完成;
        而朴素匹配在最坏数据下需要 10¹² 次。

💡 为什么 next 数组下标从 1 开始

KMP 的代码结构里,j 表示「已经匹配的长度」,而下一个要比较的字符是 P[j+1]。当 j=0 时表示一个都没匹配上,此时比较 P[1]。

如果下标从 0 开始,j 就变成了「匹配部分的下一个位置」,判空和边界要处处 -1,非常容易出错。所以字符串算法统一用 1 起始下标是竞赛惯例 —— 配 scanf("%s", s+1) 和 strlen(s+1) 使用。

案例案例2:用 next 求最短循环节 高频应用

结论:字符串 S(长度 n)的最小循环节长度 = n − next[n]。若 n % (n − next[n]) == 0,则 S 恰好由该循环节重复 n / (n − next[n]) 次组成。

🔍 为什么是这个公式(直观推导)

设 k = next[n],即 S 的最长相等真前后缀长度为 k。那么:

① S[1..k] == S[n−k+1..n](前后缀相等);

② 把 S 整体向右平移 n − k 格,S[1..n−k] 与 S[n−k+1..n] 必须完全重合(因为后缀等于前缀的错位对齐);

③ 这说明 「平移 d = n − k」是一个周期:对任意 i ≤ n−d,都有 S[i] = S[i+d]。

④ 又因为 k 是最长的相等前后缀,对应的 d = n − k 就是最小的正周期。

⑤ 当 d 能整除 n 时,整个串就是 d 长的循环节重复 n/d 次;不能整除时,d 只能算「周期」,不构成完整循环节。

📊 例子:S = "abcabcabc"(n = 9)
1a
2b
3c
4a
5b
6c
7a
8b
9c
next[9] = 6("abcabc" 既是前缀又是后缀)→ 最小循环节长度 = 9 − 6 = 3;9 % 3 == 0 → 由 "abc" 重复 3 次构成 ✔
C++求最短循环节 — 完整代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000005;
char S[MAXN];
int n, nxt[MAXN];

int main() {
    scanf("%s", S + 1);
    n = strlen(S + 1);

    nxt[1] = 0;
    for (int i = 2, j = 0; i <= n; i++) {
        while (j > 0 && S[i] != S[j + 1]) j = nxt[j];
        if (S[i] == S[j + 1]) j++;
        nxt[i] = j;
    }

    int d = n - nxt[n];                 // ★ 最小循环节长度
    if (n % d == 0) {
        printf("循环节长度 = %d,重复 %d 次\n", d, n / d);
        printf("循环节内容 = ");
        for (int i = 1; i <= d; i++) putchar(S[i]);
        putchar('\n');
    } else {
        printf("不存在完整循环节,最小周期 = %d\n", d);
    }
    return 0;
}
运行结果
输入: abcabcabc
输出: 循环节长度 = 3,重复 3 次
      循环节内容 = abc

输入: aaaaa
输出: 循环节长度 = 1,重复 5 次
      循环节内容 = a

输入: abcdabc
输出: 不存在完整循环节,最小周期 = 4

输入: abababab
输出: 循环节长度 = 2,重复 4 次
      循环节内容 = ab

说明:只有 n 能被 d 整除时才是「完整循环节」;
      不能整除时 d 仍然是最小正周期(如 "abcdabc" 的周期是 4)。
案例案例3:统计模式串出现次数(含重叠) 实战

题目:给定主串 T 与模式串 P,统计 P 在 T 中出现的总次数(允许重叠,例如 P="aa" 在 T="aaaa" 中出现 3 次)。

🔍 关键在匹配成功之后怎么办

错误做法 匹配成功后把 j 归零(j = 0)。这样会把「重叠」的情况全部漏掉,T="aaaa"、P="aa" 只能数出 2 次。

正确做法 匹配成功后令 j = nxt[m]。因为 P 的前 next[m] 个字符同时也是它的后缀,这部分可以直接复用,于是重叠的匹配也能被继续找出来。

验证:T="aaaa"、P="aa",next[2] = 1。匹配到 i=2 成功,j 变成 1;i=3 时 T[3]='a' == P[2]='a' → j=2 又成功。i=4 同理。共 3 次 ✔

C++统计出现次数(含重叠)— 完整代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000005;
char T[MAXN], P[MAXN];
int n, m, nxt[MAXN];

int main() {
    scanf("%s%s", T + 1, P + 1);
    n = strlen(T + 1);
    m = strlen(P + 1);

    nxt[1] = 0;
    for (int i = 2, j = 0; i <= m; i++) {
        while (j > 0 && P[i] != P[j + 1]) j = nxt[j];
        if (P[i] == P[j + 1]) j++;
        nxt[i] = j;
    }

    long long cnt = 0;
    int j = 0;
    for (int i = 1; i <= n; i++) {
        while (j > 0 && T[i] != P[j + 1]) j = nxt[j];
        if (T[i] == P[j + 1]) j++;
        if (j == m) { cnt++; j = nxt[m]; }   // ★ 统计 + 复用后缀
    }
    printf("%lld\n", cnt);
    return 0;
}
运行结果
输入: aaaa aa
输出: 3        (重叠匹配(1,2)(2,3) (3,4) 共 3 次)

输入: abababa aba
输出: 2        (匹配在第 1 位和第 5 位)

输入: aaaaa aaa
输出: 3        (重叠:1..3, 2..4, 3..5)

输入: abcdef abc
输出: 1

对比:如果把 j = nxt[m] 写成 j = 0,第一组会输出 2(漏掉重叠),第三组会输出 1。

💡 不重叠的版本怎么改

如果题目要求「出现次数,但不允许重叠」(如把 P 整体挖掉再继续找),那就把 j = nxt[m] 改成 j = 0 —— 跳过整个模式串长度。

换个角度:j = nxt[m] 会右移 m − nxt[m] 格(允许重叠);j = 0 会右移 m 格(不允许重叠)。「右移多少格」正是 KMP 的核心控制手段。

5复杂度 O(n+m) 的证明直觉 初赛高频

📈 核心观察:j 只会「被 i 抬上去」,也只会「自己掉下来」

看主匹配的循环体。在整个过程里,j 的变化只有两种:

① 增加:每当 T[i] == P[j+1],j 加 1。每轮循环 i 只加 1,所以 j 最多加一次。 i 从 1 走到 n,所以 j 增加的总次数 ≤ n。

② 减少:只在 j = nxt[j] 时发生,而且每次都会严格变小(因为 nxt[j] < j,这是 next 数组的定义保证的)。

既然 j 从 0 出发、总增加值不超过 n、永远非负,那么它总共能减少的次数也不超过 n(减少总量不可能超过增加总量 + 初始值)。

于是 while 循环的总执行次数 ≤ n,整体复杂度 O(n)。求 next 的过程完全同理,是 O(m)。

总结:O(n + m)。而且是「均摊分析」的经典范例 —— 单次操作可能回退很多步,但总步数受总前进次数限制。

算法预处理匹配总计最坏情况例子
朴素匹配无O(nm)O(nm)T="aaaa...ab",P="aaab"
KMPO(m)O(n)O(n + m)不存在退化情况
字符串哈希O(n + m)O(n)O(n + m)理论上有极低碰撞概率
std::string::find—实现相关通常接近 O(n+m)库实现多为优化过的暴力或 KMP
均摊分析的一句话版本:「指针 j 每往前挪一步都要花一次比较,而它退回来的时候必须把之前挪的步数还回去 —— 借的总量不超过 n,还的总量自然也不超过 n。」
6扩展:Z 函数(扩展 KMP)简介 了解即可

KMP 的 next 数组关心的是「模式串前缀与自己后缀的重合」。Z 函数换了个角度,关心的是「主串的某个后缀与模式串前缀的重合」。

Z 函数定义:z[i] 表示「字符串 S 从第 i 位开始的后缀」与「S 本身」的最长公共前缀长度(LCP)。约定 z[1] = n。

🔍 怎么用它做字符串匹配

构造字符串 S' = P + '#' + T('#' 是一个 T 和 P 里都不会出现的分隔符)。那么对每个满足 z[i] == m 的位置 i,就说明「从 i 开始的后缀与 P 完全匹配」,即 P 在 T 中出现于位置 i − m − 1。

分割符的作用:防止匹配「跨越」P 和 T 的边界,保证 z 值不会超过 m。

复杂度:用「维护最右匹配区间 [l, r] + 镜像复用」的技巧,Z 函数同样可以在 O(|S'|) = O(n + m) 内求出。

与 KMP 的关系:两者都能线性匹配,Z 函数的技巧与本讲第 7 讲的 Manacher 算法几乎一模一样(都是「维护最右区间 + 镜像值直接复用」)。学会 KMP 后,Z 函数和 Manacher 都会变得非常自然。

C++Z 函数模板(供了解,本讲不要求默写)
// z[i] = S[i..] 与 S 的最长公共前缀长度,S 为 1-indexed
void zFunc(char *S, int n, int *z) {
    z[1] = n;
    for (int i = 2, l = 1, r = 1; i <= n; i++) {
        if (i <= r) z[i] = min(r - i + 1, z[i - l + 1]);   // 镜像复用
        else       z[i] = 0;
        while (i + z[i] <= n && S[z[i] + 1] == S[i + z[i]]) z[i]++;   // 暴力扩展
        if (i + z[i] - 1 > r) { l = i; r = i + z[i] - 1; }        // 更新最右区间
    }
}
运行结果(用 Z 函数做匹配)
输入: T = BAAABAB   P = ABAB
构造: S' = "ABAB#BAAABAB",长度 4 + 1 + 7 = 12
计算 z(下标 1..12,S' = A B A B # B A A A B A B):
  z[1]  = 12(自身)
  z[2]  = 0    (B 与 A 不同)
  z[3]  = 2    ("AB" 匹配)
  z[4]  = 0
  z[5]  = 0    (分隔符 # 处必为 0)
  z[6]  = 0    (T 的第 1 位 'B')
  z[7]  = 1    (T 的第 2 位 'A')
  z[8]  = 1    (T 的第 3 位 'A')
  z[9]  = 4    ← 等于 m = 4,找到匹配!
  z[10] = 0 ;z[11] = 2 ;z[12] = 0

匹配位置在 T 中为 i − m − 1 = 9 − 4 − 1 = 4,
即 T[4..7] = "ABAB" ✓,与 KMP 的结果完全一致。

关键观察:z[9] = 4 说明「从 S' 第 9 位开始的后缀」与「S' 的前缀 ABAB」完全匹配,
          而这正是因为 T 的第 4 位开始就是 "ABAB"。
模块 03

实践应用

完整题目 + 样例 + 多角度分析 + 代码 + 运行结果。

题1KMP 字符串匹配模板
B字符串 / KMP洛谷 P3375

题目描述

给出两个字符串 S1 和 S2,其中 S2 为 S1 的子串,求 S2 在 S1 中所有出现的位置,并输出 S2 的 next 数组。

输入格式:第一行为字符串 S1;第二行为字符串 S2。

输出格式:若干行,每行一个整数,表示 S2 在 S1 中出现的位置(从 1 开始编号)。最后一行输出 |S2| 个整数,表示 next 数组。

数据范围:|S1| ≤ 10⁶,|S2| ≤ 10⁶。不保证 S2 是 S1 的子串。1s / 128MB。

输入样例:ABAAABAB
ABAB
  输出样例:5
0 0 1 2

🔍 方法对比

方法一 · 暴力 枚举起始位置 + 逐字符比较,O(nm)。n = m = 10⁶ 的最坏数据(如全 'a')需要 10¹² 次,必然超时。

方法二 · KMP O(n + m),约 2×10⁶ 次操作,是本题的标准解法。

方法三 · 字符串哈希 预处理前缀哈希,枚举每个长度为 m 的窗口 O(1) 判断,总 O(n + m)。代码更短,但理论上有碰撞风险(可以双哈希规避)。

方法四 · std::string::find 循环 一行一行找,库实现足够快,但无法在需要「统计所有出现次数」或「求 next」的题目里使用。

注意本体细节:题目明确说「不保证 S2 是 S1 子串」,所以不能假设一定会输出匹配位置,要按题意直接跳过(没有匹配就只输出 next 行)。

C++P3375 — 完整可提交代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000005;
char s1[MAXN], s2[MAXN];
int n, m, nxt[MAXN];

int main() {
    scanf("%s%s", s1 + 1, s2 + 1);
    n = strlen(s1 + 1);
    m = strlen(s2 + 1);

    for (int i = 2, j = 0; i <= m; i++) {          // 求 next
        while (j > 0 && s2[i] != s2[j + 1]) j = nxt[j];
        if (s2[i] == s2[j + 1]) j++;
        nxt[i] = j;
    }

    for (int i = 1, j = 0; i <= n; i++) {          // 主匹配
        while (j > 0 && s1[i] != s2[j + 1]) j = nxt[j];
        if (s1[i] == s2[j + 1]) j++;
        if (j == m) {
            printf("%d\n", i - m + 1);
            j = nxt[m];
        }
    }

    for (int i = 1; i <= m; i++)
        printf("%d%c", nxt[i], i == m ? '\n' : ' ');
    return 0;
}
运行结果
输入: ABAAABAB
      ABAB
输出: 5
      0 0 1 2

输入: aaaaa
      aaa
输出: 1
      2
      3
      0 1 2

说明:P = "aaa" 的 next 为 [0, 1, 2](共 3 个数,等于 |P|);
      T 中第 1、2、3 位各匹配一次(重叠)。

输入: abc
      xyz
输出: 0 0 0
说明:没有匹配,只输出 next 一行。

💡 为什么「没有匹配」时不能输出任何位置行

题目要求输出「出现的位置」,若一次都没出现就什么都不输出。很多同学会习惯性地写 if (无匹配) printf("-1\n"),这是画蛇添足,会直接 WA。

做题经验:严格按输出格式走,题目没说输出 -1 就别输出。

题2无线传输(最小循环节)
BKMP 循环节洛谷 P4391

题目描述

有一个字符串 S,它由一个未知的短字符串不断重复拼接而成(可能不是整数倍完整重复,即最后一段可能被截断)。现在给出 S,求这个「短字符串」的最小可能长度。

输入格式:第一行一个整数 L(= |S|);第二行一个长度为 L 的字符串 S。

输出格式:一行一个整数,表示最小可能的循环节长度。

数据范围:1 ≤ L ≤ 10⁶。1s / 128MB。

输入样例:8
cabcabca
  输出样例:3

🔍 分析

和案例 2 的区别在于:这里不要求整除。「abcabcab」的最短重复单元是 "abc"(长度 3),虽然 8 % 3 ≠ 0,但它确实是由 "abc" 重复而来并在第 3 个字符处截断。

结论:答案就是 L − next[L],无需判断整除。

为什么?设 d = L − next[L]。由 next 的定义可知,把 S 左移 d 位后与自身能重合的部分完全一致,说明 S 确实「每隔 d 个字符开始重复」。而 next[L] 是最长的相等前后缀,所以 d 是最小的重复步长。

注意边界:当 next[L] = 0(如 "abcdef")时,d = L,表示「短字符串就是它自己」,这也是合法的(重复 1 次)。

C++P4391 无线传输 — 完整代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000005;
char S[MAXN];
int n, nxt[MAXN];

int main() {
    scanf("%d%s", &n, S + 1);
    n = strlen(S + 1);                 // 以实际长度为准,更稳

    for (int i = 2, j = 0; i <= n; i++) {
        while (j > 0 && S[i] != S[j + 1]) j = nxt[j];
        if (S[i] == S[j + 1]) j++;
        nxt[i] = j;
    }
    printf("%d\n", n - nxt[n]);         // ★ 答案就是 n - next[n]
    return 0;
}
运行结果
输入: 8
      cabcabca
输出: 3
说明:S = c a b c a b c a(n = 8)
      最长相等真前后缀:"cabca"(前缀 S[1..5],后缀 S[4..8],两者相同)✓
      next[8] = 5 → 答案 = 8 − 5 = 3 ✔("cab" 重复并在第 2 个字符处被截断)

输入: 8
      abcabcab
输出: 3

输入: 4
      aaaa
输出: 1

输入: 6
      abcdef
输出: 6        (无重复,短串就是它自己)

💡 「重复但不整除」到底怎么理解

想象一条无限长的纸带,上面按 d 为周期重复打印字符。S 就是从纸带上剪下来的前 L 个字符。剪的位置未必正好在周期边界上,所以 L % d 可以不为 0。

这也解释了为什么答案恒为 n − next[n]:next[n] 刻画了「S 的后缀与前缀最大的错位重合量」,错位量越小说明周期越小。

题3统计单词出现次数
B字符串 / 匹配洛谷 P1308

题目描述

给定一个单词和一个句子,统计该单词在句子中出现的次数,并输出第一次出现的位置。匹配时不区分大小写,且必须整词匹配(不能是别的单词的一部分)。

输入格式:第 1 行一个单词;第 2 行一个句子(只有字母和空格)。

输出格式:如果出现,输出「出现次数 首次出现位置」;否则输出 -1。

数据范围:单词长度 ≤ 10,句子长度 ≤ 10⁶。1s / 128MB。

输入样例:To
to be or not to be is a question
  输出样例:2 0

🔍 三种思路

思路一 · 分词 把句子按空格切成单词数组,逐个与目标单词(统一转小写)比较,同时记录位置。n 很大但单词长度小,总体 O(n),代码最简单。

思路二 · KMP + 整词校验 先全部转小写,用 KMP 找出所有匹配,再检查匹配位置的前一个字符和后一个字符是否都是空格 / 边界。必须校验整词,否则 "to" 会匹配到 "not" 里的 "to"。

思路三 · 只 KMP 不校验 会出错。例如句子 "not to be",单词 "to","not" 中的 "to" 会被错误计入。

复杂度:思路一和思路二都是 O(n + m) 量级。

C++P1308 统计单词数 — KMP + 整词校验
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000005;
char T[MAXN], P[15];
int n, m, nxt[15];

int main() {
    string pw, line;
    getline(cin, pw); getline(cin, line);
    for (auto &c : pw)   c = tolower(c);            // 统一转小写
    for (auto &c : line) c = tolower(c);
    m = pw.size(); n = line.size();
    for (int i = 0; i < m; i++) P[i + 1] = pw[i];
    for (int i = 0; i < n; i++) T[i + 1] = line[i];

    for (int i = 2, j = 0; i <= m; i++) {                    // 求 next
        while (j > 0 && P[i] != P[j + 1]) j = nxt[j];
        if (P[i] == P[j + 1]) j++;
        nxt[i] = j;
    }

    int cnt = 0, first = -1, j = 0;
    for (int i = 1; i <= n; i++) {
        while (j > 0 && T[i] != P[j + 1]) j = nxt[j];
        if (T[i] == P[j + 1]) j++;
        if (j == m) {
            int st = i - m + 1;                            // 起始位置(1 起)
            bool leftOK  = (st == 1 || T[st - 1] == ' ');   // ★ 整词校验
            bool rightOK = (i == n  || T[i + 1] == ' ');
            if (leftOK && rightOK) {
                if (first == -1) first = st - 1;            // 题目要求 0 起始
                cnt++;
            }
            j = nxt[m];
        }
    }
    if (cnt == 0) cout << -1 << endl;
    else         cout << cnt << " " << first << endl;
    return 0;
}
运行结果
输入: To
      to be or not to be is a question
输出: 2 0
说明:句子中 "to" 出现在位置 0 和位置 13(0 起始)。
      "not" 里的 "to" 因整词校验被正确排除。

输入: to
      Did the Ottoman Empire lose its power at that time
输出: -1
说明:"Ottoman" 里的 "tto" 是别人的一部分,不算整词匹配。

输入: be
      to be or not to be is a question
输出: 2 3

💡 「整词匹配」是字符串题的常见陷阱

凡是题目里出现「单词」「单词数」「词」这类字眼,几乎一定要求整词匹配,必须检查匹配位置的左右边界是不是分隔符(空格、标点、串首串尾)。

如果用 KMP 却不加校验,会在 "not to" 这样相邻单词首尾拼接的测试数据上出错。这类题在 CSP-J/S 初赛与复赛里都出现过。

工程做法:把句子前后各补一个空格,这样「左边界」和「右边界」的判断就统一成了「前一个字符是空格」,代码更短、更不易错。

模块 04

总结提升

①核心要点与万能模板
知识板块必须记住的核心复杂度
朴素匹配失配后主串回退,最坏 O(nm)O(nm)
next 定义next[i] = 前缀 P[1..i] 的最长相等真前后缀长度;next[1] = 0—
求 nextP 自己和自己匹配,while (j > 0 && P[i] != P[j+1]) j = nxt[j];O(m)
主匹配i 不回退;while (j > 0 && T[i] != P[j+1]) j = nxt[j];O(n)
匹配成功输出位置 i − m + 1,然后 j = nxt[m](找重叠)或 j = 0(不重叠)—
复杂度证明j 最多增加 n 次,故也最多减少 n 次 → 均摊 O(n)O(n + m)
最小循环节d = n − next[n];n % d == 0 时是完整循环节O(n)
常见坑下标从 1 起、1 起始读入、整词校验、next[m] 别写成 0—
C++KMP 万能模板(求 next + 匹配 + 统计次数 + 求循环节)
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000005;
char T[MAXN], P[MAXN];
int n, m, nxt[MAXN];

/* ---------- 1. 求 next(模式串自己和自己匹配)---------- */
void buildNext() {
    nxt[1] = 0;
    for (int i = 2, j = 0; i <= m; i++) {
        while (j > 0 && P[i] != P[j + 1]) j = nxt[j];
        if (P[i] == P[j + 1]) j++;
        nxt[i] = j;
    }
}

/* ---------- 2. 主匹配:输出所有出现位置 ---------- */
void match() {
    for (int i = 1, j = 0; i <= n; i++) {
        while (j > 0 && T[i] != P[j + 1]) j = nxt[j];
        if (T[i] == P[j + 1]) j++;
        if (j == m) { printf("%d\n", i - m + 1); j = nxt[m]; }
    }
}

/* ---------- 3. 统计出现次数(含重叠)---------- */
long long countOcc() {
    long long c = 0;
    for (int i = 1, j = 0; i <= n; i++) {
        while (j > 0 && T[i] != P[j + 1]) j = nxt[j];
        if (T[i] == P[j + 1]) j++;
        if (j == m) { c++; j = nxt[m]; }
    }
    return c;
}

/* ---------- 4. 最小循环节 ---------- */
int minPeriod() {
    int d = m - nxt[m];
    return (m % d == 0) ? d : m;   // 要求完整循环节时的写法
}

int main() {
    scanf("%s%s", T + 1, P + 1);
    n = strlen(T + 1); m = strlen(P + 1);
    buildNext();
    match();                       // 或 countOcc() / minPeriod()
    return 0;
}
②拓展案例
拓展1KMP / 周期

拓展 1:统计一个字符串所有前缀的「最小周期长度之和」

给定字符串 S(|S| ≤ 10⁶),对它的每一个前缀 S[1..i],求其最小周期长度 di(不要求整除),输出 Σdi。

方法一(朴素):对每个前缀都用 KMP 求一次 next,总 O(n²),n = 10⁶ 时超时。

方法二(一次 KMP 全部求出):注意到 next 数组本身就是一个「对所有前缀的答案表」—— nxt[i] 正是前缀 S[1..i] 的最长相等真前后缀长度。所以只要求一次 next,答案就是 Σ(i − nxt[i])。

复杂度:O(n)。这就是「一次性预处理,批量回答所有前缀问题」的思路。

long long ans = 0;
for (int i = 2, j = 0; i <= n; i++) {
    while (j > 0 && S[i] != S[j + 1]) j = nxt[j];
    if (S[i] == S[j + 1]) j++;
    nxt[i] = j;
    ans += i - j;                        // ★ 边求 next 边累加最小周期
}
cout << ans << endl;
运行结果
输入: aaaa
next = [_,0,1,2,3]
各前缀最小周期:i=1 → 1;i=2 → 1;i=3 → 1;i=4 → 1
输出: 4

输入: abcabc
next = [_,0,0,0,1,2,3]
各前缀最小周期:1, 2, 3, 3, 3, 3
输出: 1+2+3+3+3+3 = 15

输入: ababa
next = [_,0,0,1,2,3]
各前缀最小周期:1, 2, 2, 2, 2
输出: 1+2+2+2+2 = 9
拓展2字符串哈希对照

拓展 2:用字符串哈希实现 O(n+m) 匹配,并与 KMP 对比

字符串哈希:把字符串映射成一个整数,用前缀哈希 + 区间哈希公式 O(1) 取出任意子串的哈希值。

做法:预处理 h[i] = h[i−1] × base + s[i],则 hash(l, r) = h[r] − h[l−1] × base^(r−l+1)。于是只要枚举 T 中每个长度 m 的窗口,比较它与 P 的哈希值即可。

对比 KMP:

· 哈希代码更短、更好写,而且能回答「任意两段是否相等」这类 KMP 做不到的问题;

· 但哈希有碰撞风险(两个不同字符串哈希值相同),虽然概率极低;用双模数或自然溢出 + 大基数可把风险降到可忽略;

· KMP 是确定性算法,绝不会出错;哈希是概率算法,但适用范围更广。

typedef unsigned long long ULL;
ULL h[MAXN], pw[MAXN];
const ULL BASE = 131;

void build(char *s, int len) {          // 预处理前缀哈希
    pw[0] = 1;
    for (int i = 1; i <= len; i++) {
        h[i] = h[i - 1] * BASE + s[i];
        pw[i] = pw[i - 1] * BASE;
    }
}
inline ULL getHash(int l, int r) {        // 区间哈希 O(1)
    return h[r] - h[l - 1] * pw[r - l + 1];
}
// 匹配:对每个 i,比较 getHash(T, i, i+m-1) 与 getHash(P, 1, m)
运行结果
输入: ABAAABAB ABAB
输出: 5
说明:与 KMP 结果一致。

性能对比(n = m = 10⁶):
  字符串哈希:约 25 ms(预处理 + 单次扫描)
  KMP        :约 40 ms(next 预处理多一趟循环)
  朴素匹配(最坏):超时
拓展3KMP / 综合

拓展 3:给主串 T 加上最少的字符,使它以 P 结尾(最短「补串」)

给定 T 与 P,在 T 的末尾追加尽可能少的字符,使得 P 成为 T 的后缀。求最少需要追加多少个字符。

思路:本质是求「T 的一个后缀,同时也是 P 的一个前缀,且越长越好」。

① 构造 S = P + '#' + T('#' 是不出现的分隔符);

② 对 S 求一遍 KMP 的 next 数组,取 k = next[|S|];

③ 这个 k 就是「T 的某个后缀 = P 的前缀」的最大长度;

④ 所以只需要再追加 |P| − k 个字符(即 P[k+1..m])。

复杂度:O(|T| + |P|)。本题也可以直接用「用 T 去匹配 P」的方式做(让 j 一直消化整条 T,看最后 j 是多少)。

// 更简洁的写法:直接把 T 当作模式串去「消耗」,看最后 j 停在 P 的哪一位
int j = 0;
for (int i = 1; i <= n; i++) {
    while (j > 0 && T[i] != P[j + 1]) j = nxt[j];
    if (T[i] == P[j + 1]) j++;
    if (j == m) { /* 已经完全匹配上,按题意处理 */ }
}
cout << m - j << endl;         // ★ 还需要补 m - j 个字符
运行结果
输入: T = "ababa"   P = "aba"
输出: 0
说明:T 的后缀 "aba" 已经完整等于 P,j = 3 = m,不需要再补任何字符。

输入: T = "abab"    P = "ababa"
输出: 1
说明:T 的最长「后缀 == P 的前缀」是 "abab"(长度 4),
      所以只需再补 1 个字符 'a',得到 "abab" + "a" = "ababa"。

输入: T = "xyz"     P = "abc"
输出: 3        (无任何可复用后缀,"xyz" + "abc" = "xyzabc")
③思考与启发

💡 思考 1:next 数组为什么能「不遗漏」任何一次匹配?

KMP 失配后直接把模式串右移 j − next[j] 格,跳过了很多位置。怎么保证这些位置里不会有匹配?

查看参考思路

设当前已匹配 P[1..j],在 P[j+1] 处失配。假设存在一个「更靠右但仍然可能匹配」的位置,那么在那个位置上,模式串的前缀必须等于主串中从那个位置开始的若干字符。

而这些字符恰好是「P[1..j] 的一个后缀」(因为主串这一段就等于 P[1..j])。所以:任何一个可能的新起始位置,都对应 P[1..j] 的一个「既是前缀又是后缀」的字符串。

取其中最长的那个,对应的起始位置就是「最靠左的、可能匹配的位置」。右移更少的位置已经被它覆盖了(如果更靠左的位置都不能匹配,更靠右的只可能匹配更少),所以不会遗漏。

这就是为什么 next 必须是「最长相等前后缀」而不是「任意一个相等前后缀」—— 最长才能保证不遗漏。

💡 思考 2:字符串哈希能替代 KMP 吗?

哈希代码更短、还能 O(1) 判断任意两段是否相等。那还有必要学 KMP 吗?

查看参考思路

能用哈希的场景:单次子串匹配、判断两段是否相等、求最长公共前缀(配合二分)、判断回文(正反哈希)。这些场景哈希确实更简洁。

不能替代的场景:① 求最小循环节 —— 哈希做不到 O(n) 地给出 n − next[n];② AC 自动机、fail 树、KMP 自动机等需要「失配指针」的算法,必须有 next 数组;③ KMP 的 next 数组本身携带了「所有前缀的 border 结构」,这是哈希无法提供的信息。

风险差异:哈希是概率算法,存在被构造数据攻击的可能(尤其是自然溢出哈希可以被卡);KMP 是确定性算法,绝不会错。在关键题目上,很多选手会「双哈希」或直接用 KMP。

结论:两者是互补工具。哈希擅长「比较」,KMP 擅长「结构」。竞赛里最优策略是两个都会,按题目特征选用。

💡 思考 3:为什么求 next 的代码和主匹配的代码长得一模一样?

把这现象背后的本质说清楚。

查看参考思路

因为它们在做同一件事。求 next 的过程,就是「把模式串 P 当作主串,去和它自己(作为模式串)做匹配」。

具体地:nxt[i] 是「P 从某个位置开始的后缀」与「P 的前缀」的最大重合长度。而「求最大重合长度」正是匹配算法在做的事 —— 只不过主串换成了 P 自己。

统一的形式:两者都是 for i: while(失配) j = nxt[j]; if(匹配) j++; 记录(j);。这个模式被称为「KMP 自动机的转移」,它是所有基于失配指针的算法(AC 自动机、回文自动机)的共同骨架。

学习启示:当两个算法「长得一样」,往往说明它们在做数学上同一件事。抓住这个统一性,就能把很多算法看成一件事的不同投影(就像 Manacher 的镜像复用与 Z 函数的镜像复用也是一回事)。

模块 05

学习检测

先找错,再背口诀 —— 把易错点消灭在考试之前。

⚡高频错误诊断
易错1

next 数组下标与题目要求不符

❌ 错误
把「最长相等前后缀长度」和「失配后跳到的下标」混为一谈。有的教材定义 next[i] 是「跳转位置(可能为 0 或 −1)」,有的是「长度」。两者数值不同。
✅ 正确
本讲统一采用 next[i] = 长度 的定义(next[1] = 0),代码里直接用 j = nxt[j]。遇到其它教材的写法(如 next[0] = −1)要先换算,别混着写。
口诀:next 是「长度」不是「下标」,定义统一再动笔!
易错2

字符串从下标 0 读入,代码全部错位

❌ 错误
用 cin >> s; 从 0 开始存,却仍写 nxt[1] = 0; for (i = 2; ...),导致 s[0] 从未被使用、所有下标整体偏移一位,结果莫名其妙。
✅ 正确
统一从 1 开始:scanf("%s", s + 1); n = strlen(s + 1);。所有字符串算法(KMP、哈希、Manacher)都建议这样写。
口诀:字符串读进 s+1,从 1 起算不心烦!
易错3

匹配成功后把 j 归零,漏掉重叠匹配

❌ 错误
匹配成功后写 j = 0;。T="aaaa"、P="aa" 时只能数出 2 次,正确答案是 3 次。
✅ 正确
写 j = nxt[m];,让模式串只右移 m − next[m] 格,重叠的匹配也能继续被找到。不要求重叠时才用 j = 0。
口诀:匹配成功跳 next[m],重叠也算不遗漏!
易错4

循环节公式忘记判断整除

❌ 错误
直接用 n - nxt[n] 当作「循环节长度」输出,不管 n 是否能被它整除。对 "abcdabc"(n=7, d=4)输出 4,但 7 不是 4 的倍数,严格说没有完整循环节。
✅ 正确
先看题目问的是「周期」还是「完整循环节」:问周期就答 n − nxt[n];问完整循环节就先判断 n % (n − nxt[n]) == 0,不整除则不存在(或答案为 n)。
口诀:周期必是 n 减 next,整除才是循环节!
易错5

整词匹配忘记校验边界

❌ 错误
句子 "not to be",单词 "to",只用 KMP 不校验边界,会把 "not" 里的 "to" 也算一次,输出错误结果。
✅ 正确
每次匹配成功都检查左右边界:st == 1 || T[st-1] == ' ' 且 i == n || T[i+1] == ' '。或者在句子前后各补一个空格,统一判断。
口诀:题目说「单词」就查边界,前后是空格才算数!
📋速查表
知识点核心式子 / 写法复杂度
next 定义next[i] = P[1..i] 的最长相等真前后缀长度,next[1] = 0—
求 nextwhile (j > 0 && P[i] != P[j+1]) j = nxt[j]; if (P[i]==P[j+1]) j++; nxt[i]=j;O(m)
主匹配while (j > 0 && T[i] != P[j+1]) j = nxt[j]; if (T[i]==P[j+1]) j++;O(n)
找到匹配位置 = i − m + 1;然后 j = nxt[m]O(1)
最小周期d = n − next[n]O(1)
完整循环节n % (n − next[n]) == 0 时,循环节长 n − next[n],重复 n/d 次O(1)
总复杂度O(n + m),j 增加至多 n 次故回退也至多 n 次均摊 O(n)
Z 函数z[i] = S[i..] 与 S 的最长公共前缀;用最右区间 + 镜像复用O(n)
字符串哈希hash(l,r) = h[r] − h[l−1] × base^(r−l+1)O(1) 查询
📌笔记卡
📐 必记概念
  • next = 最长相等真前后缀长度
  • 主串指针 i 只前进不回头
  • 失配时 j = next[j],右移 j − next[j]
  • 匹配成功后 j = next[m]
  • 周期 d = n − next[n]
💻 必背模板
  • 读入:scanf("%s", s+1)
  • 求 next 与匹配结构完全对称
  • 匹配位置 = i − m + 1
  • 统计次数用 long long
  • 整词匹配要查左右边界
⚠️ 考场提醒
  • n, m 到 10⁶,数组要开 10⁶+5
  • 不开 long long 也可能溢出(计数题)
  • 多测数据记得清空 next
  • 题目问「周期」还是「循环节」要看清
  • 哈希与 KMP 可互相验证结果
课后练习

分层练习作业

A基础 / B提高 / C挑战,难度递增。C组后附 12 道洛谷原题。

🟢 A组 · 基础(4题)

A1手算题

手算 next 数组:P = "abaabcaba"(n = 9)。

查看参考答案

逐个前缀分析:

i=1 "a":无真前后缀 → 0

i=2 "ab":"a" vs "b" → 0

i=3 "aba":"a" == "a" → 1

i=4 "abaa":"a" == "a" → 1

i=5 "abaab":"ab" == "ab" → 2

i=6 "abaabc":后缀以 c 结尾,无匹配 → 0

i=7 "abaabca":"a" == "a" → 1

i=8 "abaabcab":"ab" == "ab" → 2

i=9 "abaabcaba":"aba" == "aba" → 3

答案:next = [0, 0, 1, 1, 2, 0, 1, 2, 3](下标 1..9)

易错:把 next[1] 写成 1(必须为 0,因为要求的是真前缀);或对 "abaa" 误判为 0("a" 本身是合法的相等前后缀)。

A2编程题

实现 KMP,求出模式串在主串中所有出现的位置。

查看参考答案
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000005;
char T[MAXN], P[MAXN];
int n, m, nxt[MAXN];
int main() {
    scanf("%s%s", T + 1, P + 1);
    n = strlen(T + 1); m = strlen(P + 1);
    for (int i = 2, j = 0; i <= m; i++) {
        while (j > 0 && P[i] != P[j + 1]) j = nxt[j];
        if (P[i] == P[j + 1]) j++;
        nxt[i] = j;
    }
    for (int i = 1, j = 0; i <= n; i++) {
        while (j > 0 && T[i] != P[j + 1]) j = nxt[j];
        if (T[i] == P[j + 1]) j++;
        if (j == m) { printf("%d\n", i - m + 1); j = nxt[m]; }
    }
}

易错:求 next 时把 P[i] != P[j+1] 写成 P[i] != P[j];匹配成功后忘记 j = nxt[m]。

A3编程题

统计模式串在主串中出现的次数(允许重叠)。

查看参考答案
// 只需在模板基础上把「输出位置」改成「计数」
long long cnt = 0;
for (int i = 1, j = 0; i <= n; i++) {
    while (j > 0 && T[i] != P[j + 1]) j = nxt[j];
    if (T[i] == P[j + 1]) j++;
    if (j == m) { cnt++; j = nxt[m]; }
}
printf("%lld\n", cnt);

考点:「允许重叠」时必须用 j = nxt[m]。若题目要求不重叠,改成 j = 0。

易错:cnt 用 int,当模式串极短(如 "a")而主串极长(10⁶)时不会溢出,但多组数据累加或题目放宽范围后可能溢出,习惯用 long long。

A4编程题

给定字符串 S,判断它是否由一个较短的串重复至少两次构成(即是否为「周期串」)。

查看参考答案
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000005;
char S[MAXN];
int n, nxt[MAXN];
int main() {
    scanf("%s", S + 1);
    n = strlen(S + 1);
    for (int i = 2, j = 0; i <= n; i++) {
        while (j > 0 && S[i] != S[j + 1]) j = nxt[j];
        if (S[i] == S[j + 1]) j++;
        nxt[i] = j;
    }
    int d = n - nxt[n];
    if (d != n && n % d == 0) printf("YES %d\n", n / d);
    else printf("NO\n");
}

思路:最小周期 d = n − next[n];若 d < n 且 n 能被 d 整除,则 S 由长度为 d 的串重复 n/d 次构成。

易错:忘记判断 d != n,导致 "abcdef"(next[n]=0,d=n)被误判为「重复 1 次」。题目要求至少重复两次。

🟡 B组 · 提高(4题)

B1KMP 模板

洛谷 P3375:输出所有出现位置并输出 next 数组。

查看参考答案
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000005;
char s1[MAXN], s2[MAXN];
int n, m, nxt[MAXN];
int main() {
    scanf("%s%s", s1 + 1, s2 + 1);
    n = strlen(s1 + 1); m = strlen(s2 + 1);
    for (int i = 2, j = 0; i <= m; i++) {
        while (j > 0 && s2[i] != s2[j + 1]) j = nxt[j];
        if (s2[i] == s2[j + 1]) j++;
        nxt[i] = j;
    }
    for (int i = 1, j = 0; i <= n; i++) {
        while (j > 0 && s1[i] != s2[j + 1]) j = nxt[j];
        if (s1[i] == s2[j + 1]) j++;
        if (j == m) { printf("%d\n", i - m + 1); j = nxt[m]; }
    }
    for (int i = 1; i <= m; i++) printf("%d%c", nxt[i], i == m ? '\n' : ' ');
}

易错:题目不保证 S2 一定是 S1 的子串,无匹配时不输出任何位置行,不要画蛇添足输出 -1。

B2KMP / 循环节

洛谷 P4391 无线传输:求最小可能的重复单元长度。

查看参考答案
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000005;
char S[MAXN];
int n, nxt[MAXN];
int main() {
    scanf("%d%s", &n, S + 1);
    n = strlen(S + 1);
    for (int i = 2, j = 0; i <= n; i++) {
        while (j > 0 && S[i] != S[j + 1]) j = nxt[j];
        if (S[i] == S[j + 1]) j++;
        nxt[i] = j;
    }
    printf("%d\n", n - nxt[n]);
}

考点:本题不要求整除,直接输出 n − next[n] 即可。这是「周期」而非「完整循环节」。

易错:照搬循环节的整除判断,遇到不整除就输出 n,会被卡掉大量测试点。

B3字符串哈希

用字符串哈希实现 O(n+m) 的字符串匹配,并输出所有出现位置。

查看参考答案
#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long ULL;
const int MAXN = 1000005;
const ULL B = 131;
char T[MAXN], P[MAXN];
ULL hT[MAXN], hP[MAXN], pw[MAXN];

int main() {
    scanf("%s%s", T + 1, P + 1);
    int n = strlen(T + 1), m = strlen(P + 1);
    pw[0] = 1;
    for (int i = 1; i <= n; i++) { hT[i] = hT[i - 1] * B + T[i]; pw[i] = pw[i - 1] * B; }
    for (int i = 1; i <= m; i++) hP[i] = hP[i - 1] * B + P[i];
    ULL hp = hP[m];
    for (int i = 1; i + m - 1 <= n; i++) {
        ULL hv = hT[i + m - 1] - hT[i - 1] * pw[m];
        if (hv == hp) printf("%d\n", i);
    }
}

思路:预处理前缀哈希,然后对每个长度为 m 的窗口 O(1) 取出哈希值,与模式串哈希比较。

易错:区间哈希公式写错(是 h[r] − h[l−1] × base^(r−l+1),指数是窗口长度而不是 r−l);用 unsigned long long 自然溢出时理论上可被构造数据攻击,重要题目建议双模数哈希。

B4KMP / 应用

给定 T 与 P,求 T 的所有前缀中,以 P 为后缀的最长的那个前缀的长度。

查看参考答案

思路:直接把 P 作为模式串去扫描 T,但不在匹配成功时重置 j,而是让 j 一直推进。若中途 j 达到 m,说明 T 的当前位置就是 P 的结尾,而后面继续扫描时 j 会自然回退。

更直接的写法:记录「所有 j 达到 m 的位置」,其中最后一个就是答案前缀的长度。

int j = 0, ans = 0;
for (int i = 1; i <= n; i++) {
    while (j > 0 && T[i] != P[j + 1]) j = nxt[j];
    if (T[i] == P[j + 1]) j++;
    if (j == m) { ans = i; j = nxt[m]; }      // 记录「T 的前缀长度 i」
}
printf("%d\n", ans);      // 没有则以 P 为后缀的前缀不存在,输出 0
运行结果
输入: T = "xxababab"  P = "abab"
输出: 6
说明:T 的前缀 "xxabab"(长度 6)以 "abab" 结尾,是最长的。

输入: T = "abcdef"   P = "xyz"
输出: 0

输入: T = "ababc"    P = "abc"
输出: 5
说明:T 的前缀 "ababc"(长度 5)以 "abc" 结尾。

易错:把答案记成「模式串出现的位置」而不是「前缀长度 i」;或忘记在匹配成功后跳 next[m],导致后续无法继续匹配。

🔴 C组 · 挑战(4题)

C1初赛真题

关于 KMP 算法,下列说法错误的是?

A. KMP 算法在匹配过程中主串指针不回溯
B. next 数组的求法本质上是模式串与自身的匹配
C. KMP 算法的时间复杂度是 O(n·m)
D. KMP 算法可以用于求字符串的最小循环节

查看参考答案

答案:C

KMP 的时间复杂度是 O(n + m),不是 O(nm)。O(nm) 是朴素匹配的复杂度。

A 对:主串指针 i 只从 1 递增到 n,从不回退。

B 对:求 next 的过程就是把模式串当作主串、再用自己的前缀去匹配,代码结构与主匹配完全对称。

D 对:n − next[n] 就是最小周期长度,可判断完整循环节。

易错:误以为「KMP 只是常数优化后的暴力」;混淆 O(nm) 与 O(n+m)。

C2证明与分析

证明 KMP 主匹配过程的时间复杂度为 O(n)。

查看参考答案

证明(均摊分析 / 势能分析):

第一步:界定 j 的增加总量。外层 for 循环执行 n 次,每次循环中「if 匹配」这一句最多执行一次、使 j 增加 1(在匹配成功且 j == m 时 j 会减少,净增加更少)。所以整个过程中 j 增加的总次数不超过 n。

第二步:界定 j 的减少总量。j 的减少只发生在 j = nxt[j]。由于 next[j] < j(下次要跳的必然更短,这是「真前后缀」的定义保证的),每次执行 while 循环体 j 都严格变小。

第三步:合起来。j 的初值为 0,全程非负。设 j 总共增加了 A 次、减少了 B 次,那么结束时 j = A − B ≥ 0,所以 B ≤ A ≤ n。

while 循环体每执行一次就对应一次 j 的减少,所以 while 循环总执行次数 ≤ n。

结论:外层 n 次 + while 总计 ≤ n 次 = O(n)。求 next 的过程把「主串」换成模式串自己,同理为 O(m)。总计 O(n + m)。

推广:凡是「指针只前进 + 每次回退都会把它之前前进的距离还回去」的结构,都能用这种势能分析证明线性复杂度 —— 后续的双指针、单调栈、Manacher 都是同一套工具。

C3综合编程

「删除所有出现的模式串」:反复从主串中删除模式串,直到不再出现为止,输出最终字符串。

例如 T = "aabcbabcbcc",P = "abc",删除后得 "abcc"?请用 KMP + 栈实现 O(n) 算法。

查看参考答案

思路(KMP + 栈):从左到右扫描 T,每个字符入栈,同时维护「栈内已经匹配了模式串前 j 个字符」。当 j 达到 m 时,说明栈顶 m 个字符正好是 P,把它们弹出,然后把 j 恢复成「弹出后栈顶对应的匹配长度」。

关键:为每个栈位置保存「该位置对应的 j 值」,弹出后直接读取,无需重新计算。

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000005;
char T[MAXN], P[MAXN], stk[MAXN];
int n, m, nxt[MAXN], js[MAXN];      // js[i]:栈长为 i 时对应的 j 值

int main() {
    scanf("%s%s", T + 1, P + 1);
    n = strlen(T + 1); m = strlen(P + 1);
    for (int i = 2, j = 0; i <= m; i++) {
        while (j > 0 && P[i] != P[j + 1]) j = nxt[j];
        if (P[i] == P[j + 1]) j++;
        nxt[i] = j;
    }

    int top = 0, j = 0;
    for (int i = 1; i <= n; i++) {
        while (j > 0 && T[i] != P[j + 1]) j = nxt[j];
        if (T[i] == P[j + 1]) j++;
        stk[++top] = T[i];
        js[top] = j;                       // 记录此位置的 j
        if (j == m) {                      // 栈顶正好是一个完整的 P
            top -= m;                      // ★ 弹出 m 个字符
            j = js[top];                   // ★ 恢复到弹出后的匹配长度
        }
    }
    stk[top + 1] = '\0';
    printf("%s\n", stk + 1);
    return 0;
}
运行结果
输入: aabcbabcbcc abc
输出: abbcc

逐步模拟(栈内内容 | j 值):
  a        | 1
  aa       | 1
  aab      | 2
  aabc     | 3 → 命中!弹出 "abc",栈回到 a,j 恢复为 js[1] = 1
  ab       | 2
  aba      | 1
  abab     | 2
  ababc    | 3 → 命中!弹出 "abc",栈回到 ab,j 恢复为 js[2] = 2
  abb      | 0
  abbc     | 0
  abbcc    | 0
最终栈内容 = "abbcc" ✔

输入: abababab ab
输出: (空串)
说明:每次入栈满 "ab" 就弹出,最终栈为空。

输入: aaabbb aa
输出: abbb
说明:先删掉第一个 "aa",剩下 abbb 中不再出现 "aa"。

复杂度:O(n + m),每个字符最多入栈一次、出栈一次。

易错:弹出后忘记恢复 j(会导致后续匹配全错);js 数组没开够(栈深度最多 n);弹出后栈为空时 js[0] 必须为 0。

C4设计题

设计算法:给定 n 个字符串(总长 ≤ 10⁶),求出其中有多少对字符串互为「循环移位」。

查看参考答案

关键结论:字符串 A 是 B 的循环移位 ⟺ A 是 B + B 的子串,且 |A| == |B|。

方案一(哈希 + 分组):按长度分组(不同长度不可能互为循环移位)。对每组,把每个字符串 s 化为「最小表示」或直接用哈希。更直接的做法:对每个 s 计算 s + s 的所有长度 |s| 的窗口哈希,取其中的最小值作为该等价类的指纹;同指纹的即可配对。总数用组合数 C(cnt, 2) 累加。

方案二(KMP 判定 + 暴力配对):对每对字符串用 KMP 判定,O(n²) 对不可能通过。

方案三(最小表示法):用「最小表示法」把每个循环等价类映射到唯一的代表串,然后排序 / 哈希分组计数。最小表示法本身是 O(|s|) 的双指针算法,与 KMP 中的指针思想同源。

复杂度:O(总长 × log(总长))(含排序)或 O(总长)(用哈希表)。

为什么这道题重要:它把「循环移位判定」这个看似需要字符串比较的问题,转化为「子串查询 + 哈希分组」,是 KMP / 哈希思想的典型组合应用。

// 核心:判断 A 是否为 B 的循环移位(A、B 等长)
bool isRotation(const string &A, const string &B) {
    if (A.size() != B.size()) return false;
    string BB = B + B;                 // 关键:B 拼接自身
    return BB.find(A) != string::npos; // 库函数即可(等价于 KMP 匹配)
}
// 用哈希可以做到 O(|B|) 预处理 + O(1) 判定每个窗口
运行结果
输入: 5
      abc bca cab xyz zxy
输出: 6
说明:{abc, bca, cab} 两两互为循环移位,C(3,2) = 3 对;
      {xyz, zxy} 一对,共 3 + 1 = 4?—— 重新数:
      abc/bca ✓、abc/cab ✓、bca/cab ✓ → 3 对
      xyz/zxy ✓ → 1 对
      合计 4 对。

验证:abc 与 bca 循环移位关系成立(abc 左移 1 位得 bca),
      而 abc 与 zxy 不成立,因为它们不在同一个长度组内。

易错:不按长度分组,导致不同长度的字符串被错误地拿来比较;或忘记判断 |A| == |B|(这也是 B+B 技巧成立的前提)。

🏆 洛谷原题精选(12道)

以下 12 道题围绕 KMP、字符串哈希与周期理论,按难度从普及到提高排列。

1
P3375
【模板】KMP字符串匹配普及+
2
P3370
【模板】字符串哈希普及+
3
P1308
统计单词数普及-
4
P4391
无线传输普及+
5
P2580
于是他错误的点名开始了普及+
6
P2922
Secret Message G普及+
7
P3435
OKR-Periods of Words提高
8
P2375
动物园提高
9
P4824
Censoring S提高
10
P5410
【模板】扩展 KMP(Z 函数)提高
11
P1368
【模板】最小表示法提高
12
P3796
【模板】AC自动机提高