题单 · 习题解析

洛谷 P4391 [BalticOI 2009] Radio Transmission 无线传输

★★★ 正解只有一行(`n − nxt[n-1]`),而它压着**两层推理**:题面那句「S 是 S′+S′+… 的**子串**」⟺「|S′| 是 S 的一个周期」,以及「最短周期 = n − 最长 border」;⇒ 所以这一页把**题面按字面全枚举验了一遍** —— 字母表 {a,b}、长度 1~10 的全部 **2046** 个串,「一行公式 / 按周期定义暴力 / ★ 枚举所有候选 S′ 真去查子串」三条路两两一致,**0 个不同** ⇒ 那条最笨的路验的**不是算法,是读题**;★★★ 而这道题最容易挂的地方是**另一道题的正确写法**:求「最小**整**周期」时那句 `if (n % d) d = n;` 是必须的,**这道题不要求切得整齐 ⇒ 一加就错**(官方样例 `cabcabca` 的 n = 8、答案 3,**3 不整除 8**,一测就死;★ 全枚举数出来随便抓一个串有 **67.16%** 会中)⇒ [第 52 章](/ch/52-tree-diff/)那条「上一章的正确写法就是这一章的 bug」在**同一张题单内**的现场,⚠ 而[同题单的 P3435](/sol/p3435/) 要的是「**最短**非零 border」,正好又反过来 —— **三道题,三个方向**;★★★ 「一个对照档同时把两个错法打成能证的 0」:形如 `XX` 的串(X 本原)⇒ `n % d == 0` 那句不生效、且 border 恰好等于答案,⚠⚠ 而这一档**是被实测逼出来的** —— 第一版忘了要求那一段自己本原(`aaaaaa` 的 border 是 5 不是 3),量出来是 74/300 而不是 0 ⇒ **「结构性的 0」要把那个结构写全了才成立**;★ 十格「触发 ≡ 抓获」一个不差;⚠ 顺手写的那一档几乎在验零(26 个字母随机,300 轮里 **284 轮**答案就是 n);⚠ 这道题**一个部分分档都没有** ⇒ `O(n²)` 暴力一分钱不给(而同题单的 [P3375](/sol/p3375/) 暴力稳拿 70 分)

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

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

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

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

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

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

题目描述

一家无线电台需要向多位接收者发送一条信息。为了确保所有听众都能接收到, 该信息在一个连续的循环中被一遍又一遍地播放。

你将得到其中一位接收者收到的一段字符序列。已知该序列的长度至少与原信息的长度一样长。

你的任务是编写一个程序,提取出电台发送的原信息。更形式化地说,你的程序需要找到输入序列 S最短子序列 S′,使得 S 本身又是(足够长的)重复序列 S′+S′+···+S′ 的子串

输入格式

第一行包含一个整数 L,即序列 S 的长度。第二行包含恰好 L 个字符,即序列 S 本身。 该序列由小写字母组成。

输出格式

一行,一个整数:信息 S′ 的长度 L′。请注意,L′ 必须是尽可能小的值。

规模与约定

对于全部的测试点,保证 1 ≤ L ≤ 10⁶。时限 1 秒,内存 128000 KB(125 MB)。

输入输出样例

输入

8
cabcabca

输出

3

abc 不断自我连接得到 abcabcabcabc,读入的 cabcabca 是它的子串 ⇒ 答案 3。

⚠⚠ 注意这一组:n = 8,答案 3,而 3 并不整除 8 —— 第 ③ 步整节都在说这件事。 ★ 另外注意 S′ = abc 不是 S 的前缀Sc 开头):题面要的是「子串」,不是「前缀」。

1★ 一句话:答案 = n − 最长 border

★★ 一行代码,压着两层推理
   printf("%d\n", n - nxt[n - 1]);

这一行成立,靠的是两句话,每一句都得单独站住

  1. S 是某个长度 d 的串重复无限次的子串」⟺「dS 的一个周期」 (周期的定义:对所有 i ≥ ds[i] == s[i-d])。
    • ⇐ 有周期 ds[i] = s[i mod d]S 就是「前 d 个字符」重复串的前缀,当然是子串;
    • S′^∞ 整体有周期 d,而子串继承周期S 有周期 d
    • ⚠ 题面要的是子串不是前缀(样例里 S′ = abc 就不是 S 的前缀), 但上面这两半只谈长度,所以不影响答案。
  2. 最短周期 = n − 最长 border(border 和周期是同一件事的两种说法: 长度 b 的 border ⟺ 长度 n − b 的周期)。

⇒ 第 ② 步把这两句话按字面全枚举验了一遍,因为「一行代码 + 两层推理」正是最该验的形状。

p4391.cpp★ 正解:一行
// P4391 无线传输 —— 正解:答案 = n − nxt[n-1],一行
//
// ★★ 两步就到:
// ① 题面那句「S 是 S'+S'+… 的**子串**」⟺ **|S'| 是 S 的一个周期**
// (周期 d 的定义:对所有 i ≥ d 有 s[i] == s[i−d])。
// · ⇐ 有周期 d ⇒ s[i] = s[i mod d] ⇒ S 就是 (前 d 个字符)^∞ 的前缀,当然是子串;
// · ⇒ S'^∞ 整体有周期 d,而**子串继承周期** ⇒ S 有周期 d。
// ⚠ 注意题面要的是「子串」不是「前缀」—— S' 不一定是 S 的前缀(`cabcabca` 的 S' 可以是 `abc`),
// 但**最短的那个长度是同一个**,所以不影响答案(p4391All.cpp 里全枚举验过)。
// ② 最短周期 = **n − 最长 border**(border 和周期是一一对应的两种说法)。
//
// ⚠⚠ 而这道题最容易挂的地方,是把另一道经典题的写法搬过来:
// 「最小**整**周期」那类题(要求 d 整除 n)会多写一句 `if (n % d) d = n;`
// —— 这道题**不要求整除**(样例 `cabcabca` 的答案就是 3,而 3 不整除 8)⇒ 那一句一加就错(见 p4391Div.cpp)。
#include <cstdio>
#include <cstring>
using namespace std;
static const int MAXN = 1000006;
static char s[MAXN];
static int nxt[MAXN];
int main() {
int n;
if (scanf("%d", &n) != 1) return 0;
if (scanf("%s", s) != 1) return 0;
n = (int)strlen(s);
nxt[0] = 0;
for (int i = 1, j = 0; i < n; i++) {
while (j > 0 && s[i] != s[j]) j = nxt[j - 1];
if (s[i] == s[j]) j++;
nxt[i] = j;
}
printf("%d\n", n - nxt[n - 1]);
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
p4391Brute.cpp★ 参照物:按周期的定义硬验(不碰 next)
⚠ 这道题一个部分分档都没有 ⇒ 暴力一分钱都拿不到

题面只有一句「1 ≤ L ≤ 10⁶」,没有任何子任务。 ⇒ 上面那份 O(n²) 的参照物在真题上是 10¹² 次比较,一分不给 (和第 39 章 P1531 / P1198 那两道一样 —— ⚠ 而同一张题单里的 P3375 的暴力稳拿 70 分「暴力值多少分」是「暴力 × 那道题分档」的属性)。

★ 正解那一侧的账:顶格 10⁶ 本机 不到 0.01 秒(读入 1 MB + 一趟 next),余量 100 倍以上。

2★★★ 把题面那句话按字面全枚举验一遍

p4391All.cpp★ 三条路:一行公式 / 周期定义 / ★ 按题面字面枚举候选 S′
// P4391 —— 把题面那句「是 S'+S'+… 的**子串**」按字面全枚举验一遍。
// ./p4391All 人话版
// ./p4391All csv 给 check:viz 用
//
// ★★★ 为什么要有这一份:正解只有一行(`n − nxt[n-1]`),而它压着**两层推理**:
// ① 「S 是某个长度 d 的串重复无限次的子串」⟺「d 是 S 的一个周期」;
// ② 「最短周期」= 「n − 最长 border」。
// ⇒ 两层都对才轮得到那一行。而[「验算要走一条和算法完全无关的路」](/sol/p1332/)——
// 这里走的是**最笨的那条**:把长度 d 的**候选串全枚举一遍**,
// 真的去查「S 是不是它重复若干次之后的子串」。
//
// ⚠ 顺带数一个决定这一页对拍档位的数:**有多少个串会让「整除版」出错**
// (也就是 n 不被答案整除的那些)—— 那正是 p4391Div.cpp 的触发条件。
#include <bits/stdc++.h>
using namespace std;
static const int ALPHA = 2; // 字母表 {a, b}
static const int LMAX = 10; // 串长 1..10
static int byKmp(const string& s) {
int n = (int)s.size();
vector<int> nxt(n, 0);
for (int i = 1, j = 0; i < n; i++) {
while (j > 0 && s[i] != s[j]) j = nxt[j - 1];
if (s[i] == s[j]) j++;
nxt[i] = j;
}
return n - nxt[n - 1];
}
static int byPeriod(const string& s) { // 按「周期」的定义
int n = (int)s.size();
for (int d = 1; d <= n; d++) {
bool ok = true;
for (int i = d; i < n && ok; i++) if (s[i] != s[i - d]) ok = false;
if (ok) return d;
}
return n;
}
static int byLiteral(const string& s) { // ★ 按题面**字面**:枚举候选 S',查 S 是不是它的重复串的子串
int n = (int)s.size();
for (int d = 1; d <= n; d++) {
int total = 1; for (int i = 0; i < d; i++) total *= ALPHA;
for (int code = 0; code < total; code++) {
string t;
int c = code;
for (int i = 0; i < d; i++) { t.push_back((char)('a' + c % ALPHA)); c /= ALPHA; }
string rep;
while ((int)rep.size() < n + d) rep += t; // 重复到够长
if (rep.find(s) != string::npos) return d;
}
}
return n;
}
int main(int argc, char** argv) {
bool csv = argc > 1 && string(argv[1]) == "csv";
long long total = 0, bad12 = 0, bad13 = 0, notDivide = 0, halfBorder = 0;
for (int len = 1; len <= LMAX; len++) {
long long cnt = 1;
for (int i = 0; i < len; i++) cnt *= ALPHA;
for (long long code = 0; code < cnt; code++) {
string s;
long long c = code;
for (int i = 0; i < len; i++) { s.push_back((char)('a' + c % ALPHA)); c /= ALPHA; }
total++;
int a = byKmp(s), b = byPeriod(s), d = byLiteral(s);
if (a != b) bad12++;
if (a != d) bad13++;
if (len % a) notDivide++; // ⇒ 「整除版」在这个串上会答错
int nb = len - a;
if (nb * 2 == len) halfBorder++; // ⇒ 「只打 border」那版恰好蒙对
}
}
if (csv) {
printf("total,%lld\nbadKmpPeriod,%lld\nbadKmpLiteral,%lld\n", total, bad12, bad13);
printf("notDivide,%lld\nhalfBorder,%lld\n", notDivide, halfBorder);
printf("pctNotDivide,%.2f\npctHalf,%.2f\n",
100.0 * (double)notDivide / (double)total, 100.0 * (double)halfBorder / (double)total);
return 0;
}
printf("① 字母表 {a,b}、长度 1~%d 的全部 %lld 个串,三种算法两两一致:\n", LMAX, total);
printf(" n − 最长 border ↔ 按「周期」定义暴力 对不上 %lld 个\n", bad12);
printf(" n − 最长 border ↔ ★ 按题面**字面**全枚举候选 S' 对不上 %lld 个\n", bad13);
printf("② 其中 n **不被**答案整除的有 %lld 个(%.2f%%)—— 「整除版」正是在这些串上答错\n",
notDivide, 100.0 * (double)notDivide / (double)total);
printf("③ 其中最长 border 恰好是一半的有 %lld 个(%.2f%%)—— 「只打 border」那版只在这些串上蒙对\n",
halfBorder, 100.0 * (double)halfBorder / (double)total);
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果
★★★ 第三条路是最笨的那条 —— 而它验的正是「题面 → 公式」这一步

n − nxt[n-1] 和「按周期定义暴力」都还在同一套语言里(周期 / border)。 ⇒ 要验的其实是上一层:题面说的是「SS′^∞子串」,这跟周期是一回事吗?

★ 所以第三条路干脆不讲道理:把长度 d候选串全枚举一遍 (字母表 {a,b}2^d 个),真的去查「S 是不是它重复若干次之后的子串」。

字母表 {a,b}、长度 1~10 的全部 2046 个串:

两两比较 对不上
n − 最长 border ↔ 按周期定义暴力 0 个
n − 最长 border ↔ ★ 按题面字面枚举候选 S′ 0 个

「验算要走一条和算法完全无关的路」的又一次, ★ 而这一次那条路验的不是算法,是读题 —— 它证明「子串」和「周期」在这道题上真的是一回事。

同一次全枚举还顺手数出了两个决定对拍的数:

个数 / 2046 占比
n 不被答案整除的串 1374 67.16%
最长 border 恰好是一半的串 52 2.54%

★★ 第一行就是下一步那个错法的触发面:三分之二的串会让「整除版」答错。

3★★★ 错法一:多写了一句「d 必须整除 n」—— 那是另一道题的正确写法

★★★ 同一个 n − nxt,两道题差一句话

求「最小周期」(把 S 恰好切成若干个完全相同的段)时,标准写法是:

   int d = n - nxt[n-1];
   if (n % d) d = n;            // ★ 那道题里这一句是**必须**的

⇒ 而这道题不要求切得整齐 —— 只要 SS′^∞ 的子串, S′ 可以在中间开始、也可以在中间结束。那一句一加就错。

★ 官方样例一测就死:cabcabcan = 8d = 33 不整除 8 ⇒ 它打出 8。 ★ 触发条件:n 不被答案整除(上一步数过:随便抓一个串,67.16% 会中)。

⇒ ★★★ 这是第 52 章立的那条「上一章的正确写法可能就是这一章的 bug」 在同一张题单内的现场 —— ⚠ 而更狠的还在后面: 同一张题单里的 P3435 要的是「最短非零 border」, 和这道题的「最长 border」正好相反。三道题,三个方向。

p4391Div.cpp✗ 错法一:加了「必须整除」那一句
// P4391 ✗ 错法一:多写了一句「d 必须整除 n」
//
// ⚠⚠ 这是**另一道题的正确写法**:求「最小**整**周期」(把 S 恰好切成若干个相同的段)时,
// `d = n − nxt[n-1]` 只有在 `n % d == 0` 时才是答案,否则整个串没有整周期、答案是 n。
// 那条判断在那类题上是必须的 —— ★ 而这道题**只要求 S 是 S'^∞ 的子串,不要求切得整齐**。
// ⇒ 官方样例一测就死:`cabcabca` 的 n = 8、d = 3,**3 不整除 8**,它会打出 8。
// ★ 触发条件:**n 不被 (n − nxt[n-1]) 整除**。
#include <cstdio>
#include <cstring>
using namespace std;
static const int MAXN = 1000006;
static char s[MAXN];
static int nxt[MAXN];
int main() {
int n;
if (scanf("%d", &n) != 1) return 0;
if (scanf("%s", s) != 1) return 0;
n = (int)strlen(s);
nxt[0] = 0;
for (int i = 1, j = 0; i < n; i++) {
while (j > 0 && s[i] != s[j]) j = nxt[j - 1];
if (s[i] == s[j]) j++;
nxt[i] = j;
}
int d = n - nxt[n - 1];
if (n % d) d = n; // ⚠ 这一句是「最小整周期」那道题的
printf("%d\n", d);
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

4⚠ 错法二:把 border 本身当成了答案

★ 触发条件是一个等式:2 × nxt[n-1] ≠ n

「答案和 next 有关」是对的,可有关的是 n 减去它。 直接打 nxt[n-1] 等于在答「最长 border 有多长」——那是另一个问题。

★ 它只在「最长 border 恰好是一半」时蒙对(abab 那种)—— 上一步全枚举数过:2046 个串里只有 52 个(2.54%)

p4391Nxt.cpp✗ 错法二:打的是 border

5★★★ 对拍:十格「触发 ≡ 抓获」,而一个对照档同时把两个错法打成能证的 0

p4391Gen.cpp★ 生成器:档 4 是专门造出来的对照档
// P4391 的生成器:./p4391Gen 种子 [档位]
//
// ★ 两个错法各靠什么现形:
// · p4391Div(多写了「d 必须整除 n」)→ **n 不被答案整除**;
// · p4391Nxt(直接打 border) → **2 × nxt[n-1] ≠ n**(border 不是正好一半)。
//
// 档位:
// 0 ★ 顺手写的:26 个小写字母随机,n = 10~40(⚠ 几乎必然没有 border ⇒ 答案就是 n)
// 1 ⚠ 字母表压到 2 个(a / b)⇒ border 开始有了
// 2 ⚠⚠ **整周期串**:一小段重复整数次(n = d × k)
// ⇒ ★ 那一档 `n % d == 0`,「整除版」那一句**根本不生效** ⇒ 它是**能证的精确的 0**
// 3 ★ 最终档:周期串 + **随机截断**(n 不再整除 d)⇒ 「整除版」当场现形
// 4 ⚠⚠ 对照档:一段**正好重复两次**(`XX` 这种形状)⇒ 最长 border 恰好是一半
// ⇒ ★★ 「整除版」和「只打 border」**同时**变成能证的精确的 0(两个 0 各有一行证明)
//
// ⚠ 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 >= 1) ? 2 : 26;
auto pick = [&]() { unsigned r = rng() % (unsigned)alpha; return (char)('a' + r); };
string s;
if (mode >= 2) {
unsigned rd = rng() % 5u;
int d = 2 + (int)rd; // 一段的长度 2~6
unsigned rk = rng() % 5u;
int k = 3 + (int)rk; // 重复 3~7 次
if (mode == 4) k = 2; // ⚠ 对照档:正好两次 ⇒ border = n/2
string unit;
for (;;) { // 摇一段出来
unit.clear();
for (int i = 0; i < d; i++) unit.push_back(pick());
if (mode != 4) break;
/* ⚠ 档 4 要的是「border 恰好一半」,那要求这一段**自己没有更短的周期**
(比如 unit = "aaa" 时,"aaaaaa" 的最长 border 是 5 而不是 3)*/
bool primitive = true;
for (int q = 1; q < d && primitive; q++) {
if (d % q) continue;
bool per = true;
for (int i = q; i < d && per; i++) if (unit[(size_t)i] != unit[(size_t)(i - q)]) per = false;
if (per) primitive = false;
}
if (primitive) break;
}
for (int i = 0; i < k; i++) s += unit;
if (mode == 3) { // ⚠ 随机截断:n 不再是 d 的倍数
unsigned rc = rng() % (unsigned)d;
int cut = (int)rc;
if (cut) s.resize(s.size() - (size_t)cut);
}
} else {
unsigned r = rng() % 31u;
int n = 10 + (int)r;
for (int i = 0; i < n; i++) s.push_back(pick());
}
printf("%d\n%s\n", (int)s.size(), s.c_str());
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果

五档 × 300 轮(正解 vs 周期定义暴力:1500 轮 0 组不一致):

档位 答案 = n 的轮数 ✗ 必须整除 ✗ 打 border
0 ★ 顺手写的(26 个小写字母随机) 284 16 300
1 ⚠ 字母表压到 2 个 88 212 300
2 ⚠ 整周期串(一段重复 3~7 次) 0 0 300
3 ★ 最终档(周期串 + 随机截断) 0 175 300
4 ⚠⚠ 对照档:一段正好重复两次 0 0 0
★★★ 三条读得出来的结论
  1. ★★★ 档 4 把两个错法同时打成 0,而两个 0 都能不跑程序地证明: 那一档的串形如 XXX 自己没有更短的周期)⇒ n = 2d、最长 border 正好是 d

    错法 为什么在 XX 上必然对
    必须整除 n = 2dn % d == 0 ⇒ 那一句根本不生效
    打 border border = d,而答案 = n − d = d两个数恰好相等

    第 33 章 P1266第 47 章 P1200 / P1598 那条的又一次: 一个对照档可以同时给两个 0 交代清楚。 ⚠⚠ 而这一档是被实测逼出来的:第一版只写「一段重复两次」,忘了要求那一段自己是本原的 —— unit = "aaa""aaaaaa" 的最长 border 是 5 不是 3, 于是「打 border」那一列量出来是 74 / 300 而不是 0。 ⇒ ★★ 「结构性的 0」要把那个结构写全了才成立第 42 章 P1965 那条)。

  2. ★★ 顺手写的那一档几乎在验零 —— 26 个小写字母随机,300 轮里有 284 轮答案就是 n (随机串几乎没有周期)⇒ 「整除版」只抓到 16 次。 ⇒ 而把字母表压到 2 个,同一列立刻变成 212 —— 第 22 章 P1020 那条「生成器该照抄题面的比值」在字符串题上的老形态。

  3. 十格「触发 ≡ 抓获」一个不差n % 答案 ≠ 02 × nxt[n-1] ≠ n

6★ 哪一版就已经能过了

★ 正解只有一行,而这一页的功课全在那一行之外
版本 结果 说明
p4391.cpp AC 顶格 10⁶ 不到 0.01 秒
p4391Brute.cpp 0 分 没有任何部分分档,O(n²) = 10¹²
✗ 必须整除 WA 样例就死;随便抓个串 67% 会错
✗ 打 border WA 每一组都错(除非 border 恰好一半,2.54%)

⇒ ★★ 一句话带走:这道题的代码是全书最短的之一,而它值得单独占一页的理由有两个 —— ① 那一行压着两层推理,而第二层(题面「子串」⟺ 周期)只能靠按字面全枚举去验; ② 它和「最小周期」那道经典题只差一句 if (n % d) d = n;而那一句在这里是错的 —— ⇒ 背下来的模板,要连它的前提一起背第 43 章第 44 章 P2142 那条,换到字符串上又成立一次)。