← 目录 第 52 / 60 节 全部 60 节

52 · KMP 与字符串匹配

用 fail 数组把“失配后从哪重新比”预先算好:f[i] 是前缀 s[1..i] 的最长 border,失配时沿 border 链跳,主串指针永不回退,匹配 O(n+m)。同一套下标还能直接读出最小周期 n − f[n],Z 函数则用 [l, r] 盒子在 O(n) 内给出每个后缀与前缀的 LCP。
提高预计 70 分钟KMPborderZ 函数
本节出现的新函数 / 新语法(先看懂再用)

在长串 s1 里找模式串 s2 的所有出现位置,暴力做法是:把 s2 对到每个起点上,逐字符比。最坏 O(|s1|·|s2|)——s1 = s2 = 106 个 a 时是 1012 次比较,必然超时。KMP 把“失配之后从哪一位重新开始比”预先算进一个数组,于是 s1 的指针永不回退,总时间 O(|s1| + |s2|)。

失配时不需要把 s2 挪一格重来:已经匹配上的那一段,既等于 s2 的前一段,也等于 s1 的末尾,所以只要知道“s2 的这段前缀里,最长的、同时也是后缀的前缀有多长”——这个长度就是 fail。
任务做法复杂度关键代码
s2 是否出现在 s1(暴力)每个起点逐字符比O(|s1|·|s2|)双层 for
s2 在 s1 的所有位置KMPO(|s1|+|s2|)i 不回退,j = f[j]
每个前缀的最长 border对自己做一次 KMPO(|s2|)f[i] = j
最短循环节n − f[n]O(n)一行减法
所有 border沿 f 链跳O(链长)j = f[j]
每个后缀与前缀的 LCPZ 函数O(n)盒子 [l, r]

先把这一节的 4 样新东西拆开。核心只有 5 行,但要把 5 行写对,得先知道每一行在维护什么。

新函数拆解 · 1 基下标读入:cin >> (s + 1)
cin >> (s + 1); int n = strlen(s + 1);
组成含义 / 格式要求
为什么用 1 基f[i] 的含义是“前 i 个字符”,下标天然从 1 数。用 0 基写要处处减一,border 题差一位就整题错。
cin >> (s + 1)读进以 s[1] 开头的位置,s[0] 空着不用。括号不能省:cin >> s + 1 会被解析成 (cin >> s) + 1。
strlen(s + 1)从 s[1] 数到 '\0',得到的就是串长。数组至少开 maxlen + 2(留 s[0] 与结尾 '\0')。
1e6 的串char s[1000005] 是 1 MB。全局数组在静态区,不占栈;写在 main 里是栈上 1 MB,容易爆栈。
多组数据记得清 f 数组。f 是按长度用的,只要重新算一遍 f[1..m] 就够,不必 memset 全部。
最小可运行示例
示例
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 1000005;
char s[MAXN];                  // s[1] 是第一个字符,s[0] 闲置

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> (s + 1);            // 读到 s[1] 起始的位置
    int n = strlen(s + 1);     // 串长
    cout << n << " " << s[1] << " " << s[n] << "\n";
    return 0;
}
// 输入 abcde → 输出 5 a e
注意

1 基下循环写 for (int i = 1; i <= n; ++i),i 就是“第 i 个字符”,不需要在脑子里做 i−1 的换算。这是字符串算法最省心的写法。

数组开小了不一定报错:少 1 个字节时 '\0' 写到数组外,属于未定义行为,可能本地没事、交上去 RE。

新函数拆解 · fail 数组(next 数组):五行的递推
f[1] = 0; for (int i = 2, j = 0; i <= m; ++i) { while (j && s[i] != s[j + 1]) j = f[j]; if (s[i] == s[j + 1]) ++j; f[i] = j; }
组成含义 / 格式要求
f[i] 是什么s 的前 i 个字符(记为 s[1..i])的最长 border 长度。border = 既是真前缀又是真后缀的子串。
f[1] = 0单个字符没有非本身的 border。这是整条递推的起点,也是所有“回退到底”的终点。
j 的含义进入第 i 轮时 j = f[i−1],并且 s[1..j] 与 s[i−j..i−1] 已经相等。这两段相等是“可以直接接着比”的全部依据。
if (s[i] == s[j + 1])已经对齐的两段后面再接一个相同字符,border 就延长一位:f[i] = j + 1。
while (j && s[i] != s[j + 1])失配就往短里找下一个候选。候选只能是 f[j]、f[f[j]]……一路跳到 0,跳到底再比 s[1]。
j 必须写在 for 的初始化里写成 for (int i = 2; i <= m; ++i) { int j = 0; ... } 每轮归零,f 全变 0。
j 从哪来循环开始时 j = 0 = f[1],之后每轮结束 j = f[i],正好是下一轮需要的值。
最小可运行示例
示例
const int MAXN = 1000005;
int f[MAXN];

// s 从 s[1] 开始,长度 m
void buildFail(const char* s, int m) {
    f[1] = 0;                                  // 单个字符没有 border
    for (int i = 2, j = 0; i <= m; ++i) {      // j 在这里声明:跨轮保留
        while (j && s[i] != s[j + 1]) j = f[j]; // 失配:跳到次长 border
        if (s[i] == s[j + 1]) ++j;              // 匹配:border 延长 1
        f[i] = j;
    }
}

// s = "aabaaab" → f[1..7] = 0 1 0 1 2 2 3
注意

f[i] 永远不会超过 f[i−1] + 1。因为 f[i] 的 border 去掉最后一位就是 s[1..i−1] 的 border,长度必然 ≤ f[i−1]。这条性质决定了 while 里“往短跳”是唯一的出路。

多测题里 f 数组的元素会被逐个覆盖(i 从 1 到 m 全写一遍),所以不需要额外清空。

新函数拆解 · KMP 匹配:i 不回退的扫描
for (int i = 1, j = 0; i <= n; ++i) { while (j && s1[i] != s2[j + 1]) j = f[j]; if (s1[i] == s2[j + 1]) ++j; if (j == m) { /* 起点 i-m+1 */ j = f[j]; } }
组成含义 / 格式要求
i扫 s1 的指针,从左到右走一遍,永不回退。这是 O(n) 的来源。
j当前已经匹配的字符数。它同时是 s2 的下标:下一个要比的是 s2[j+1]。
while (j && ...)与建 fail 时同一行代码。KMP 匹配就是“用 s2 的 fail 信息驱动 s1 的扫描”。
if (j == m)完整匹配一次,出现位置是 i − m + 1(1 基)。
j = f[j]★ 不能写 j = 0。s2 = aa、s1 = aaa 时,位置 1 和 2 都该输出。
总复杂度i 走 n 步;j 每步最多加 1,故 while 总次数 ≤ n,整体 O(n + m)。
最小可运行示例
示例
for (int i = 1, j = 0; i <= n; ++i) {
    while (j && s1[i] != s2[j + 1]) j = f[j];   // 失配:沿 fail 后退
    if (s1[i] == s2[j + 1]) ++j;                // 这一位对上了
    if (j == m) {                               // 整串对上
        cout << i - m + 1 << "\n";              // 起点(1 基)
        j = f[j];                               // 允许重叠,继续找
    }
}
注意

s2[j + 1] 在 j = m 时会读到 s2[m+1] = '\0'——只有忘记在匹配后回退 j 才会发生。

整个算法不需要对 s1 做任何预处理,也不需要 s1 的 fail 数组。流式数据(边读边匹配)也能用。

新函数拆解 · Z 函数(另一种“与前缀的匹配长度”)
z[1] = n; for (int i = 2; i <= n; ++i) { z[i] = 0; if (i <= r) z[i] = min(z[i - l + 1], r - i + 1); while (i + z[i] <= n && t[1 + z[i]] == t[i + z[i]]) ++z[i]; if (i + z[i] - 1 > r) { l = i; r = i + z[i] - 1; } }
组成含义 / 格式要求
z[i] 是什么t[1..] 与 t[i..] 的最长公共前缀长度。z[1] = n(自己和自己)。
l, r当前“最靠右”的已知匹配段 [l, r]:t[l..r] 与 t[1..r−l+1] 相同。
i ≤ r 时先抄盒子内的部分已经对齐,所以 z[i] 至少是 min(z[i−l+1], r−i+1)。
再暴力往外扩抄完之后继续逐字符比。这一步看着像暴力,实际总代价是 O(n)。
为什么是 O(n)r 只增不减,每次 while 成功都让 r 右移,而 r ≤ n。总扩大次数 ≤ n。
更新盒子i + z[i] − 1 > r 说明右边界推远了,换上新的 l、r。
最小可运行示例
示例
const int MAXN = 2000005;
int z[MAXN];

void getZ(const char* t, int n) {          // t 从 t[1] 开始
    z[1] = n;
    int l = 1, r = 0;
    for (int i = 2; i <= n; ++i) {
        z[i] = 0;                                      // 清掉上一次运行的值
        if (i <= r) z[i] = min(z[i - l + 1], r - i + 1); // 盒子内先抄
        while (i + z[i] <= n && t[1 + z[i]] == t[i + z[i]]) ++z[i];
        if (i + z[i] - 1 > r) { l = i; r = i + z[i] - 1; }
    }
}

// t = "aaa" → z = 3 2 1
注意

min 里的 r − i + 1 是“别抄出盒子”。少了它会用到还没验证的位置,答案偏大。

Z 函数配 t = s2 + '#' + s1 就能做匹配:z[i] ≥ |s2| 的位置就是一个出现。分隔符必须在字符集之外。

例题 · 【模板】KMP洛谷 P3375普及/提高-去洛谷做这题 ↗

给出两个字符串 s1、s2。若 s1 的区间 [l, r] 子串与 s2 完全相同,则称 s2 在 s1 中出现了,出现位置为 l。求 s2 在 s1 中所有出现的位置。

另外,对 s2 的每个前缀,求出它的最长 border 长度(border 指“不是 s 本身”的、既是 s 的前缀又是 s 的后缀的子串)。

输入:第一行字符串 s1,第二行字符串 s2。

输出:先每行一个整数,按从小到大输出所有出现位置;最后一行输出 |s2| 个整数,第 i 个是 s2 长度为 i 的前缀的最长 border 长度。

数据范围:1 ≤ |s1|, |s2| ≤ 106,只含大写英文字母。

输入: ABABABC ABA 输出: 1 3 0 0 1
例题怎么想:先让 s2 认识自己

暴力匹配的浪费在哪:设 s2 已经和 s1 对上了 j 个字符,第 j+1 个失配。暴力会把 s1 的起点右移一格、j 清零重来,把刚刚确认过的 j 个字符又比一遍。n = m = 106、s1 = s2 = 全是 a 时,浪费是 1012 级别的。

关键观察:失配那一刻,我们已知 s1[i−j..i−1] = s2[1..j]。新的对齐方式相当于问:s2 的前缀 s2[1..k] 能不能同时等于 s2[1..j] 的后缀?如果能,就让 k 个字符“自动对上”,从 k+1 继续比。

“既是 s2[1..j] 的前缀、又是它的后缀”就是 border 的定义。满足条件的 k 恰好是 j、f[j]、f[f[j]]……一条链。挑最大的那个 k,最省事,也不会漏解。所以失配后的动作是 j = f[j],而不是 j = 0。

于是“跳到哪”只由 s2 自己决定,跟 s1 无关。把它预先算出来:让 s2 和自己做一次同样的匹配,得到 f[1..m]。这一步 O(m)。

正式匹配时 i 只管往前走,j 按 fail 调整,总调整次数不超过 n,整题 O(n + m)。匹配成功后写 j = f[j](此时 j = m),因为“允许重叠”,下一次出现可能从当前位置往前一点点开始。

两段几乎一样的循环
① 预处理:对 s2 做 KMP,得到 f[1..m]
     f[1] = 0; j = 0
     for i = 2..m:
         while j > 0 且 s2[i] != s2[j+1]: j = f[j]
         if s2[i] == s2[j+1]: j++
         f[i] = j

② 匹配:扫一遍 s1
     j = 0
     for i = 1..n:
         while j > 0 且 s1[i] != s2[j+1]: j = f[j]
         if s1[i] == s2[j+1]: j++
         if j == m: 输出 i-m+1;  j = f[j]

③ 输出 f[1..m]

看清楚一件事:② 和 ① 是同一段代码,只是把“自己”换成了别的串。所以 KMP 只要写对一次,另一处照抄即可。

完整代码
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 1000005;
char s1[MAXN], s2[MAXN];
int f[MAXN];                       // f[i]:s2 前 i 个字符的最长 border 长度

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> (s1 + 1) >> (s2 + 1);   // 1 基下标
    int n = strlen(s1 + 1), m = strlen(s2 + 1);

    // ① 预处理:s2 匹配自己,求出 f[1..m]
    f[1] = 0;                                  // 单字符没有非本身的 border
    for (int i = 2, j = 0; i <= m; ++i) {
        while (j && s2[i] != s2[j + 1]) j = f[j];   // 失配:沿 border 链后退
        if (s2[i] == s2[j + 1]) ++j;                // 对上了:border 延长 1
        f[i] = j;
    }

    // ② 匹配:i 扫 s1,j 记录已匹配长度
    for (int i = 1, j = 0; i <= n; ++i) {
        while (j && s1[i] != s2[j + 1]) j = f[j];
        if (s1[i] == s2[j + 1]) ++j;
        if (j == m) {                          // 出现一次
            cout << i - m + 1 << "\n";
            j = f[j];                          // 允许重叠,退到最长 border 继续
        }
    }

    // ③ 输出 f[1..m],空格分隔,最后一个后换行
    for (int i = 1; i <= m; ++i) {
        cout << f[i];
        cout << (i == m ? "\n" : " ");
    }
    return 0;
}
逐行拆解
代码片段含义与理由
cin >> (s1 + 1) >> (s2 + 1);两个串都从下标 1 开始存。括号必须有,否则 >> 的优先级低于 +,意思就变了。
int n = strlen(s1 + 1), m = strlen(s2 + 1);1e6 的串求长度是 O(n),只做一次。此后所有下标都用 1..n。
f[1] = 0;递推的起点。若忘了这一句,f[1] 保留旧值,第一轮 while 就会用错数字。
for (int i = 2, j = 0; i <= m; ++i)j 写在初始化部分:它在轮与轮之间保留,正好承担“上一轮的 border 长度”。
while (j && s2[i] != s2[j + 1]) j = f[j];★ 整题的核心。一次跳过一个候选,跳的步数是均摊 O(1) 的。
if (s2[i] == s2[j + 1]) ++j;注意比的是 s2[j+1] 而不是 s2[j]:j 是“已对齐的长度”,下一个待比的下标是 j+1。
f[i] = j;把这一轮的结果存下来。下一轮直接用,不用重算。
for (int i = 1, j = 0; i <= n; ++i)匹配阶段。i 从 1 走到 n,一次也不回退。
if (j == m) { cout << i - m + 1 ...起点 = 终点 − 长度 + 1。i 是 s1 里匹配段的最后一个字符。
j = f[j];★ 写成 j = 0 会漏掉重叠的出现。f[m] 是 s2 的最长 border,也是下一次可能的重叠长度。
cout << (i == m ? "\n" : " ");最后一行 |s2| 个整数,行末换行、中间一个空格。格式错一样 WA。
fail 的递推为什么这么写

这是全节唯一需要动脑的地方。设 f[i−1] = j,含义是 s[1..j] = s[i−j..i−1]。要算 f[i],只有三种情况。

“f[i] ≤ f[i−1] + 1”的理由:若 s[1..k] 是 s[1..i] 的 border,去掉最后一位,s[1..k−1] 就是 s[1..i−1] 的 border,所以 k−1 ≤ f[i−1],即 k ≤ f[i−1] + 1。这条性质说明“长度每轮最多加 1”,也说明失配时只可能往短里找。

所以 while (j && ...) j = f[j]; 不是“随便往回退一点”,而是沿着 all-border 链按从大到小枚举候选。链上的值恰好是 j, f[j], f[f[j]], …, 0,既不会漏,也不会重复。

is[1..i]这一轮发生了什么f[i]
1a起点0
2aaj=0,s[2]=s[1] → j=11
3aabj=1,s[3]≠s[2] → j=f[1]=0;s[3]≠s[1]0
4aabaj=0,s[4]=s[1] → j=11
5aabaaj=1,s[5]=s[2] → j=22
6aabaaaj=2,s[6]≠s[3] → j=f[2]=1;s[6]=s[2] → j=22
7aabaaabj=2,s[7]=s[3] → j=33

第 6 行是重点:j 从 2 跳到 1 不是“减一”,而是跳到 f[2]。如果这里写成 j−−,就退化成 O(nm)。

对照上面那张表再看这三行
代码片段含义与理由
while (j && s[i] != s[j + 1]) j = f[j];情况 2:沿 border 链找下一个候选。j 变成 f[j] 之后,s[1..j] 仍然等于 s[i−j..i−1],只是变短了,所以可以继续比 s[j+1]。
if (s[i] == s[j + 1]) ++j;情况 1:延长。注意此时若 j 曾跳到 0,比的是 s[1],也就是从最小的 border 重来。
f[i] = j;情况 3 的出口:j 已经是 0 且 s[1] ≠ s[i] 时不再进入 if,f[i] = 0。

fail 数组的第二大用途是求周期。一个串 s(长 n)的周期 p 指:对所有 i + p ≤ n 都有 s[i] = s[i+p]。等价说法是“s 有长度 n − p 的 border”。于是最小周期 = n − f[n]。

周期与 border 是一一对应的:想让周期最小,就让 border 最长,而最长 border 就是 f[n]。
周期、完整循环节、全部 border
    // 假设 s[1..n] 与 f[1..n] 已经算好
    int p = n - f[n];                       // 最小周期
    // 完整循环节:n 能整除 p 时,s 由 n/p 个相同的块拼成
    int blocks = (n % p == 0) ? n / p : 0;  // 0 表示“不完整周期”
    cout << p << " " << blocks << "\n";

    // 全部 border 长度(从长到短):沿 f 链走
    for (int k = f[n]; k > 0; k = f[k])
        cout << k << " ";
周期公式的三处用法
代码片段含义与理由
int p = n - f[n];f[n] 是 s 的最长 border。去掉“最长 border 与前缀重合的那一段”,剩下的就是最小移动量 p。
n % p == 0 ? n / p : 0整除才是“若干个完整块”。如 abababa 的 p = 2,但 7 % 2 ≠ 0,它只是“不完整周期”——末尾被截断了。
for (int k = f[n]; k > 0; k = f[k])border 链。f[k] < k 恒成立,所以循环一定终止;但链长最坏是 n−1(全是同一字符时),别在每个位置都遍历整条链。
为什么对应若 p 是周期,则 s[1..n−p] = s[p+1..n],即长度 n−p 的前缀等于同长度后缀——正是 border。反之亦然。

Z 函数是 KMP 的“兄弟”:它直接给出“每个后缀与整个串前缀的最长公共前缀”。把模式串拼在主串前面、中间加一个不在字符集里的分隔符,一次 Z 就能得到所有匹配位置。

Z 函数做匹配
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 2000005;              // |s1| + |s2| + 1 再留余量
char t[MAXN];
int z[MAXN];

void getZ(int n) {                     // t 从 t[1] 开始
    z[1] = n;
    int l = 1, r = 0;
    for (int i = 2; i <= n; ++i) {
        z[i] = 0;
        if (i <= r) z[i] = min(z[i - l + 1], r - i + 1);   // 盒子内先抄
        while (i + z[i] <= n && t[1 + z[i]] == t[i + z[i]]) ++z[i];
        if (i + z[i] - 1 > r) { l = i; r = i + z[i] - 1; }
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    string a, b;
    cin >> a >> b;                     // a 主串,b 模式串
    int n = a.size(), m = b.size();

    for (int i = 1; i <= m; ++i) t[i] = b[i - 1];
    t[m + 1] = '#';                    // 分隔符:不在字符集里,跨界的匹配立刻失败
    for (int i = 1; i <= n; ++i) t[m + 1 + i] = a[i - 1];

    int N = m + 1 + n;
    getZ(N);
    for (int i = m + 2; i <= N; ++i)
        if (z[i] >= m) cout << i - m - 1 << "\n";   // 起点 = i - (m + 1)
    return 0;
}
盒子 [l, r] 里先抄再扩
代码片段含义与理由
t = b + '#' + a分隔符保证“跨过分隔符的 LCP”不会虚增。于是 z[i] ≥ m 恰好等价于“b 在 a 的位置 i−(m+1) 出现”。
z[i] = min(z[i - l + 1], r - i + 1)盒子内 t[i−l+1..] 与 t[i..] 已经对齐,能抄 z[i−l+1] 这么多;但抄不能越过盒子右端,所以再取 r−i+1。
while (i + z[i] <= n && ...)从抄来的长度继续往外扩。边界条件 i + z[i] ≤ n 防越界。
if (i + z[i] - 1 > r)只有右端被推远才换盒子。r 单调不减,所以总扩大次数 ≤ n,整体 O(n)。
i - m - 11 基下 a 的第 pos 个字符在 t 中的下标是 m+1+pos,反过来 pos = i − (m+1)。
为什么还学 Z求“每个后缀与前缀的 LCP”Z 一步到位;求 border 链 fail 更直接。两个都写熟,按题目挑。
KMP 的 6 种错法(带症状)

WA,样例就错、fail 全为 0。—— j 声明在循环体内(for (int i = 2; ...) { int j = 0; ... }),每轮归零,f 永远算不出非零值。

RE / 越界。—— 数组按 |s1| 开。1 基下标要额外留出 s[0] 和结尾 '\0',必须开 |s1| + 2 以上;写 char s1[1000000] 装 106 个字符一定爆。

WA,漏掉重叠的出现。—— 匹配成功后写 j = 0。s2 = aa、s1 = aaa 时应输出 1 和 2 两个位置,写 0 只出 1 个。正确写法是 j = f[j]。

TLE。—— 失配时写成 --j 或 j = f[j-1] 之类的“退一格”版本,退化成 O(nm)。106 的对 a 串能跑出 1012 次比较。

WA,答案整体差 1。—— 0 基写法与 1 基写法混用。网上有 nxt[i] 表示 s[0..i] 的 border 的版本,含义与本节 f[i] 差一位,两套不能混。

WA,格式错。—— P3375 要求每个位置单独一行,最后一行才是 fail 数组;不少模板题是位置空格分隔在一行。先读输出格式再写输出。

自己动手复现

关掉页面,把 P3375 从空文件敲一遍并 AC。

改一改 ①:把 while (j && ...) j = f[j]; 换成 if (j && ...) --j;,输入 s1 为 106 个 a、s2 为 1000 个 a,用 time 看超时差多少。

改一改 ②:把 j = f[j] 改成 j = 0,输入 aaa 与 aa,位置从两个变成一个。

改一改 ③:用 n − f[n] 做周期题。输入 cabcabca 得 3;输入 abababa 得 2,且 7 % 2 ≠ 0——它是“不完整周期”。

改一改 ④:把 Z 函数的 min(z[i-l+1], r-i+1) 改成 z[i-l+1],对拍 P3375 的输出,找出第一个出错的位置。

数学补给 · 均摊 O(n) 与 border 链

两个循环形状相同:j 每轮最多 +1,while 里每次至少 −1。设 j 的总增量 Δ+、总减少量 Δ−。因为 j ≥ 0 恒成立,有 Δ− ≤ Δ+;而 ++j 每轮至多执行一次,故 Δ+ ≤ n。于是 while 的总次数 ≤ n,整个匹配 O(n)。这种“把代价记到未来的增量上”的证明叫势能分析,j 就是势能。

两个恒等式,所有 border 题都从它们出发:

border 链没有对数界:s = an 时链就是 n−1, n−2, …, 1,长度 n−1。所以不能靠“链短”来保证复杂度。KMP 的 O(n) 来自势能分析:整场扫描里 +1 至多 n 次,因此 −1 至多 n 次,与某一次跳多远无关。这也解释了为什么“沿链跳”本身很便宜,而在每个位置都重新遍历整条链(例如求所有前缀的全部 border)会变成 O(n2)。

完整周期与不完整周期:n % (n − f[n]) == 0 时 s 由 n/(n−f[n]) 个相同的块拼成;不整除时只是“不完整周期”,如 abababa 的最小周期是 2,但 7 不是 2 的倍数。题目问“循环节个数”还是“周期长度”,答案完全不同。

Fine–Wilf 定理:若 p、q 都是 s 的周期且 p + q − gcd(p, q) ≤ |s|,则 gcd(p, q) 也是 s 的周期。它说明“两个周期会互相挤压出更小的周期”,是很多周期结论的来源。

Z 与 fail 的分工:z[i] 回答“从 i 开始的后缀,与整串开头能对上多少位”;f[i] 回答“前 i 个字符这一段里,前缀与后缀最长能重合多少”。一个以“位置”为轴,一个以“长度”为轴。要“每个位置与前缀的匹配长度”用 Z 更直接;要“前缀的 border 链、周期”用 fail 更直接。

举一反三 · 想清楚这些再往下走
  1. f 数组只存了“最长 border”。如果要 O(1) 回答“某个前缀的全部 border”,需要额外存什么?总空间是多少?
  2. 把 s2 + '#' + s1 拼起来求一次 fail,能不能直接读出所有匹配位置?和“边扫边匹配”比,代价差在哪?
  3. 求 s 的所有循环节长度。为什么它们一定是 n − f[n] 的倍数?给出证明或反例。
  4. 给一个串 s,最少补几个字符能让它变成“某个串重复至少两次”?先用 n − f[n],再想整除条件该怎么接。
  5. 怎么用 KMP 求“每个前缀在 s 中作为子串出现了多少次”?提示:建 fail 树,从长到短把计数累加到 f[i] 上。
  6. Z 函数和 KMP 都能做匹配。举一个 Z 更顺手、一个 fail 更顺手的例子,并说明为什么。
自测:能全部回答再进下一节
挑战题 · Radio Transmission 无线传输
洛谷 P4391提高-去洛谷做这题 ↗

电台把一条信息 S′ 反复拼接后循环播放:S′S′S′……。你收到的是其中一段连续的字符序列 S,长度 L 保证不小于 |S′|。求 S′ 的最小可能长度。

形式化地说:求 S 的最短子串 S′,使得 S 本身是无限串 S′S′S′…… 的一个子串。

输入:第一行一个整数 L(1 ≤ L ≤ 106);第二行恰好 L 个小写字母,即序列 S。

输出:一行一个整数,表示 S′ 的最小长度。

引导性提示(先只看这里)

“S 是 S′ 无限重复的一段”等价于:存在 p,使得对每个满足 i + p ≤ L 的 i 都有 S[i] = S[i+p]。这个 p 有个名字,叫周期。

周期的“补集”是 border:p 是周期,当且仅当 S 有长度 L − p 的 border。想让 p 最小,就要让 border 最长。

先手算样例 cabcabca:最长的、既是前缀又是后缀的真前缀有多长?把它从 L 里减掉,得到什么?再想想为什么不需要判断整除。

想看思路了?点开完整思路

记 p 为 S 的一个周期:S[i] = S[i+p] 对所有合法 i 成立。那么 S 去掉最后 p 个字符得到的前缀,与去掉最前 p 个字符得到的后缀完全相同,即 S 有长度 L−p 的 border。反过来,有长度 b 的 border 就等价于有周期 L−b。

所以“最小周期” ⟺ “最长 border”。最长 border 就是 fail 数组的最后一项 f[L]。答案 = L − f[L]。

为什么不需要整除判断:题目允许最后一段 S′ 被截断。样例 cabcabca 的 p = 3,8 不是 3 的倍数,但它确实是 abcabcabcabc 的一段子串。若要求“完整循环节个数”,才需要额外判 L % p == 0。

复杂度:只需求一次 f[1..L],O(L) 时间、O(L) 空间,L = 106 轻松通过。

边界:L = 1 时 f[1] = 0,答案 1;S 全为同一字符时 f[L] = L−1,答案 1。两端都对。

还是不会?点开题解代码与拆解
参考代码
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 1000005;
char s[MAXN];
int f[MAXN];

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int L;
    cin >> L;
    cin >> (s + 1);                    // 1 基

    f[1] = 0;
    for (int i = 2, j = 0; i <= L; ++i) {
        while (j && s[i] != s[j + 1]) j = f[j];   // 沿 border 链后退
        if (s[i] == s[j + 1]) ++j;
        f[i] = j;
    }

    // 最小周期 = 长度 - 最长 border
    cout << L - f[L] << "\n";
    return 0;
}
关键片段拆解
片段作用
cin >> (s + 1);题目先给长度 L 再给串,读到 s[1] 起始位置,后面所有下标都是 1 基,与 f 的定义对齐。
f[1] = 0;单字符没有非本身的 border。L = 1 时答案就是 1 − 0 = 1。
for (int i = 2, j = 0; i <= L; ++i)j 写在初始化里,跨轮保留 f[i−1] 的值,这是递推能接上的关键。
while (j && s[i] != s[j + 1]) j = f[j];失配跳 border 链。整段均摊 O(L),不会退化。
cout << L - f[L] << "\n";★ 全部答案。最长 border 越长,周期越短;不判整除,因为末段允许被截断。
这题最容易错的地方
  • 加了 if (L % (L - f[L]) == 0) 才输出,样例之外的数据全错——本题允许不完整周期。
  • 数组开成 char s[1000000]:1 基下标 + 结尾 '\0' 需要 106+2 个字节,会越界 RE。
  • 把答案写成 L - f[L - 1]:f 的下标是“前几个字符”,最后一项必须是 f[L]。
  • 用 0 基读入(cin >> s)却照抄 1 基的递推,整题位移一位。
  • 把 f[i] 算成“包含本身的 border”(f[L] = L):答案会变成 0。border 必须是真子串,递推里 f[1] = 0 就是这条约束的起点。
延伸阅读 / 参考