题单 · 习题解析

洛谷 P3435 [POI 2006] OKR-Periods of Words

★★★ 最难的一步在写代码之前:把题面那句「`A` 是 `QQ` 的前缀」翻成 **`0 < b ≤ L/2`**,再看出「要**最大**周期 = 要**最短非零** border」(而 `b ≤ L/2` 是白送的 —— 若最短非零 border > L/2,它自己重叠会给出更短的一个);★★★ 而这里和[同题单的 P4391](/sol/p4391/) **正好相反**:那道要 `n − ` **最长** border,这道要 `L − ` **最短非零** border ⇒ **同一张题单、同一个 next,两道题往相反方向用**,照抄隔壁的公式在官方样例上把 24 打成 **12**;★★★ 「求最短非零 border」根本不用跳链,一句递推就够(`f[i] = f[nxt[i]-1] ? f[nxt[i]-1] : nxt[i]`)—— 而所有人的第一版「沿链一路跳到底」**答案永远对**、只是 O(n²):全 `a` 串上跳 498 501 / 7 994 001 / **127 976 001** 步(n 每翻 4 倍 **×16.0**,O(n²) 的签名),⚠⚠ 而**随机串那一列是精确的 0** —— 顺手造一组顶格随机跑一遍,这个坑一步都看不见;★★★ `int` 那条线是 **n = 65537**(全 a 串答案 `n(n−1)/2`)—— ⚠ 和[第 11 章 P1908](/sol/p1908/)、[第 35 章 P2866](/sol/p2866/) 量到的**一模一样**,**它是 `int` 的性质不是这道题的性质**;而那条线**不能拿同一个公式再算一遍来自检**,确认是拿真程序在 65536 / 65537 两侧各跑一遍(int 版在 65537 打出 −2 147 450 880);★★★ 这一页最值钱的一条:**四个待测版本里有两个对拍永远抓不到,而两个 0 完全不是一回事** —— 一个是「答案永远对」(换尺子数次数),一个是「档位到不了那条线」(算一句 + 在线两侧跑真程序)⇒ **看到一列全是 0,先问是哪一类**;★ 四格「触发 ≡ 抓获」一个不差,⚠ 而顺手写的那一档 300 轮只抓到 **1** 次(字母表压到 2 个就是 262)

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

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

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

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

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

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

题目描述

一个字符串是由小写英文字母组成的有限序列。特别地,它也可以是空序列(即长度为 0 的序列)。

如果字符串 A 是通过字符串 BC 按顺序连接得到的,我们表示为 A = BC

如果存在一个字符串 B 使得 A = PB,那么字符串 P 是字符串 A前缀。 此外,如果 P ≠ AP 不是空字符串,我们称 PA真前缀

如果 QA 的真前缀,并且 A 是字符串 QQ 的前缀(不一定是真前缀),那么字符串 QA 的周期。 例如,字符串 ababababab 都是 abababa 的周期。

字符串 A最大周期是其最长的周期,如果 A 没有周期,则为空字符串。 例如,ababab 的最大周期是 abababc 的最大周期是空字符串。

任务:编写一个程序,计算该字符串所有前缀的最大周期长度之和

输入格式

第一行包含一个整数 k,表示字符串的长度。接下来的一行包含一个由 k 个小写英文字母组成的字符串。

输出格式

单独一行输出一个整数,表示输入字符串所有前缀的最大周期长度之和。

数据范围

对于所有数据,1 ≤ k ≤ 10⁶。时限 1 秒,内存 131072 KB(128 MB)。

输入输出样例

输入

8
babababa

输出

24

babababa 的八个前缀里,前两个(bba)没有周期,各贡献 0; 后六个分别贡献 2 / 2 / 4 / 4 / 6 / 6 ⇒ 合计 24

1★★ 第一步是翻译:题面那个「周期」,比常说的周期多一个条件

★★★ 两行推导,把题面变成「最短非零 border」

|A| = L|Q| = q。题面对 Q 的要求是两条:

  1. QA 的真前缀0 < q < L
  2. AQQ 的前缀 ⇒ ① L ≤ 2qQQ 得够长);② A[i] = A[i-q](也就是通常说的「q 是周期」)。

而「周期 q」⟺「border b = L − q」,把三条翻译过去就是:0 < b ≤ L/2

⇒ 要最大的 q,就是要最小的非零 b

★ 而那个 b ≤ L/2白送的,不用管: 若最短的非零 border b > L/2,那条 border 自己会重叠,于是 2b − L 也是一个更短的非零 border —— 和「它最短」矛盾。⇒ 最短非零 border 一定 ≤ L/2。

⇒ 于是答案 = Σ (L − 最短非零 border),没有非零 border 的前缀贡献 0。

★★★ 而这里和隔壁那道题正好相反 —— 同一个 next,两个方向
要的是 一行公式
P4391 无线传输 最短周期 n − 最长 border
本题 最大周期 L − 最短非零 border

同一张题单、同一个 next 数组,两道题往相反的方向用。 ⚠ 把这道题写成隔壁那道的样子(ans += (i+1) - nxt[i]),算出来的是 每个前缀的最小周期之和 —— 官方样例上 24 变成 12(第 ③ 步)。

★★ 加上 P4391 那一页量到的「多写一句 if (n % d) d = n; 就变成另一道题」, 这一章一口气给了三个方向: ⇒ 第 52 章那条「上一章的正确写法可能就是这一章的 bug」, 在同一张题单内连着成立两次。

2★ 正解:一句递推替掉「沿链跳到底」

★ 求「最短非零 border」根本不用跳

f[i] = s[0..i] 的最短非零 border 长度(没有就是 0)。 nxt[i] 本身是一个 border,而比它更短的 border,正好就是「nxt[i] 那个前缀」的 border

   f[i] = nxt[i] == 0        ? 0
        : f[nxt[i]-1] != 0   ? f[nxt[i]-1]     // border 的 border 更短
        : nxt[i];                              // nxt[i] 自己就是最短的

一趟 O(n),每个下标只碰一次。 ⚠ 而所有人的第一版都是「每个前缀沿 border 链一路跳到底」—— 那是 O(n²),第 ⑤ 步量它。

p3435.cpp★ 正解:一趟 next + 一句递推
// P3435 OKR-Periods of Words —— 正解:每个前缀的最大周期 = 长度 − **最短非零 border**
//
// ★★ 先把题面那个「周期」翻译清楚(它和常说的「周期」差一个条件):
// 题面:Q 是 A 的**真前缀**,且 A 是 **QQ** 的前缀。设 |A| = L、|Q| = q,那么
// · 「A 是 QQ 的前缀」⇒ ① L ≤ 2q;② A 有通常意义的周期 q(即 A[i] = A[i−q])。
// · 「真前缀」⇒ 0 < q < L。
// 而周期 q ⟺ border b = L − q ⇒ 三个条件合起来就是:**0 < b ≤ L/2**。
// ⇒ 要最大的 q,就是要**最小的非零 border**。
// ★ 而「最短非零 border 一定 ≤ L/2」是白送的:若 b > L/2,那条 border 自己重叠,
// 于是 2b − L 也是一个更短的非零 border ⇒ 最短的那个不可能 > L/2。
//
// ⚠⚠ 这里和[同一张题单的 P4391](/sol/p4391/) **正好相反**:那道题要的是 **n − 最长 border**。
// ⇒ 同一个 next 数组,两道题往两个方向用(见 p3435Long.cpp —— 那一版就是把这道题当成了那道题)。
//
// ★ 求「最短非零 border」不用跳链,一条递推就够(O(n)):
// f[i] = s[0..i] 的最短非零 border 长度(没有就是 0)
// = nxt[i] == 0 ? 0
// : f[nxt[i]-1] != 0 ? f[nxt[i]-1] // border 的 border 更短
// : nxt[i]; // nxt[i] 自己就是最短的那个
// ⚠ 答案顶格 ≈ n²/2 = 5×10¹¹ ⇒ **必须 long long**(见 p3435Int.cpp,那条线正好在 n = 65537)。
#include <cstdio>
#include <cstring>
using namespace std;
static const int MAXN = 1000006;
static char s[MAXN];
static int nxt[MAXN], f[MAXN];
int main() {
int k;
if (scanf("%d", &k) != 1) return 0;
if (scanf("%s", s) != 1) return 0;
int 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;
}
long long ans = 0;
for (int i = 0; i < n; i++) {
int b = nxt[i];
if (b == 0) f[i] = 0;
else f[i] = f[b - 1] ? f[b - 1] : b;
if (f[i]) ans += (long long)(i + 1) - f[i]; // 没有非零 border ⇒ 周期是空串,加 0
}
printf("%lld\n", ans);
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
p3435Brute.cpp★ 参照物:按题面的定义硬试每个 q
★ 那两层翻译是验过的

上面那份参照物不碰 next、不提 border,直接按题面问: 「Q = A[0..q-1] 是真前缀吗?AQQ 的前缀吗?」——第一个成立的 q 就是最大周期。

⇒ 四档 1200 轮,和正解 0 组不一致。 ★ 它验的不是算法,是读题「验算要走一条和算法完全无关的路」)。

3⚠ 错法一:用了最长 border —— 那是隔壁 P4391 的写法

★ 触发条件:存在某个前缀,它的 border 链长度 ≥ 2

「最长 border」和「最短非零 border」只有在 border 链只有一节时才是同一个东西。 ⇒ 触发条件就是它的反面:存在某个前缀,nxt[nxt[i]-1] 也非零

⚠ 而这件事在顺手写的随机数据上几乎不发生 —— 26 个小写字母随机造的串,nxt 几乎全是 0,300 轮里只有 1 轮能抓到它(第 ⑤ 步那张表)。 ★ 官方样例倒是一测就死(24 → 12)。

p3435Long.cpp✗ 错法一:最长 border(=隔壁那道题的公式)
// P3435 ✗ 错法一:用了**最长** border —— 那是隔壁 P4391 的写法
//
// ⚠⚠ 同一个 next 数组,两道题往**相反**的方向用:
// · [P4391](/sol/p4391/):要**最短周期** = n − **最长** border;
// · 本题: 要**最大周期** = L − **最短非零** border。
// ⇒ 顺手写成 `ans += (i+1) - nxt[i]`,算出来的是**每个前缀的最小周期之和**。
// ★ 触发条件:**存在某个前缀,它的最长 border 和最短非零 border 不是同一个**
// (也就是 border 链长度 ≥ 2)。⇒ 官方样例 `babababa` 一测就死(24 → 20)。
#include <cstdio>
#include <cstring>
using namespace std;
static const int MAXN = 1000006;
static char s[MAXN];
static int nxt[MAXN];
int main() {
int k;
if (scanf("%d", &k) != 1) return 0;
if (scanf("%s", s) != 1) return 0;
int 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;
}
long long ans = 0;
for (int i = 0; i < n; i++) if (nxt[i]) ans += (long long)(i + 1) - nxt[i]; // ⚠ 最长而不是最短
printf("%lld\n", ans);
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

4⚠ 错法二:答案用 int —— 而那条线是一个眼熟的整数

p3435Count.cpp★ int 那条线 + 跳链步数
// P3435 解析页上那几个「对拍看不见」的数字的出处。
// ./p3435Count 人话版
// ./p3435Count csv 给 check:viz 用
//
// 它回答三件事:
// ① ★ **`int` 那条线在哪** —— 全是同一个字母时答案是 n(n−1)/2,第一次越过 2³¹−1 的 n 是多少。
// ⚠ 这里**只负责算**;确认那一步交给 `check:viz`:拿真程序在 n = 65536 / 65537 上
// 各跑一遍,看 int 版是不是**正好**在 65537 开始和正解不一样。
// (拿同一个公式再算一遍不叫自检 —— [第 19 章 P2240](/sol/p2240/) 那条。)
// ② 顶格(n = 10⁶)的答案有多大、是 `int` 上限的多少倍;
// ③ ★★★ 「沿 border 链一路跳到底」那一版**跳了多少步** ——
// 它的答案永远对,对拍看不见,**只能换尺子数次数**
// ([第 20 章 P5019](/sol/p5019/) 那条:n 每翻 4 倍,O(n²) 的签名是次数 ×16)。
#include <bits/stdc++.h>
using namespace std;
/** 全 a 串上,「一路跳到底」要跳多少步 */
static long long jumpSteps(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;
}
long long steps = 0;
for (int i = 0; i < n; i++) {
int j = nxt[i];
if (!j) continue;
while (nxt[j - 1]) { j = nxt[j - 1]; steps++; }
}
return steps;
}
/** 正解那一版(一句递推)只花几步 —— 每个下标恰好一次 */
static long long recurSteps(const string& s) { return (long long)s.size(); }
int main(int argc, char** argv) {
bool csv = argc > 1 && string(argv[1]) == "csv";
/* ① int 那条线:算出来 + 穷举确认 */
long long INTMAX = 2147483647LL;
int lineCalc = 0;
for (long long n = 1; n <= 200000; n++)
if (n * (n - 1) / 2 > INTMAX) { lineCalc = (int)n; break; }
long long atLine = (long long)lineCalc * (lineCalc - 1) / 2;
long long belowLine = (long long)(lineCalc - 1) * (lineCalc - 2) / 2;
/* ② 顶格 */
long long big = 1000000LL * (1000000LL - 1) / 2;
/* ③ 跳链步数:全 a 串 vs 随机串 */
mt19937 rng(20260908u);
vector<int> NS = {1000, 4000, 16000};
vector<long long> stepA, stepR;
for (int n : NS) {
stepA.push_back(jumpSteps(string((size_t)n, 'a')));
string r;
for (int i = 0; i < n; i++) { unsigned c = rng() % 26u; r.push_back((char)('a' + c)); }
stepR.push_back(jumpSteps(r));
}
if (csv) {
printf("lineCalc,%d\natLine,%lld\nbelowLine,%lld\n", lineCalc, atLine, belowLine);
printf("big,%lld\novInt,%.0f\n", big, (double)big / (double)INTMAX);
for (size_t i = 0; i < NS.size(); i++) printf("stepA%d,%lld\n", NS[(int)i], stepA[i]);
for (size_t i = 0; i < NS.size(); i++) printf("stepR%d,%lld\n", NS[(int)i], stepR[i]);
printf("ratioA1,%.1f\nratioA2,%.1f\n",
(double)stepA[1] / (double)stepA[0], (double)stepA[2] / (double)stepA[1]);
printf("recur,%lld\n", recurSteps(string(16000, 'a')));
return 0;
}
printf("① int 那条线:全 a 串的答案 n(n−1)/2 第一次越过 2³¹−1 是在 n = %d\n", lineCalc);
printf(" (n = %d 时 %lld ≤ 2147483647,n = %d 时 %lld > 它)\n",
lineCalc - 1, belowLine, lineCalc, atLine);
printf(" ⚠ 这只是算出来的;确认那一步在 check:viz 里:拿真程序在这两个 n 上各跑一遍\n");
printf(" ⇒ 和第 11 章 P1908、第 35 章 P2866 的那条线一模一样 —— 它是 int 的性质\n");
printf("② 顶格 n = 10⁶:答案 %lld,是 int 上限的 %.0f 倍\n", big, (double)big / (double)INTMAX);
printf("③ 「一路跳到底」跳了多少步(全 a 串 / 随机串):\n");
for (size_t i = 0; i < NS.size(); i++)
printf(" n = %5d:全 a %12lld 步 随机 %6lld 步\n", NS[(int)i], stepA[i], stepR[i]);
printf(" n 每翻 4 倍,全 a 那一列 ×%.1f、×%.1f —— 这就是 O(n²) 的签名\n",
(double)stepA[1] / (double)stepA[0], (double)stepA[2] / (double)stepA[1]);
printf(" ★ 而正解那一句递推:每个下标恰好走一次,n = 16000 就是 %lld 步\n", recurSteps(string(16000, 'a')));
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果
★★ 65537 —— 这个数第三次出现了

全是同一个字母时,长度 L 的前缀贡献 L − 1 ⇒ 答案 = Σ (L−1) = n(n−1)/2

n(n−1)/2 第一次越过 2³¹−1 n = 65537
n = 65536 时 2 147 450 880 ≤ 2 147 483 647;n = 655372 147 516 416 > 它)
顶格 n = 10⁶ 的答案 499 999 500 000,是 int 上限的 233 倍

★★★ 而 65537 这个数,第 11 章 P1908(逆序对)和 第 35 章 P2866(能看见几头牛)量到的是同一个 —— 三道题毫无关系,可它们的上界都是 n(n−1)/2。 ⇒ 它是 int 的性质,不是那道题的性质(和 46341 那个数一样)。

⚠⚠ 而「算出来的那条线」不能拿同一个公式再算一遍来自检第 19 章 P2240 那条)—— 所以确认那一步是拿真程序跑的:全 a 串,n = 65536 两版都打 2 147 450 880, n = 65537 正解打 2 147 516 416、int 版打 −2 147 450 880线正好在那儿。

p3435Int.cpp✗ 错法二:答案用 int

5★★★ 慢法:沿 border 链一路跳到底 —— 答案永远对,只能数次数

★★ 而「顶格随机」这时候一步都看不见

「一路跳到底」和正解的答案逐字节相同,对拍三万轮也抓不到。 ⇒ 换尺子,数「一共跳了多少步」:

n 全是 a 随机 26 个字母
1 000 498 501 0
4 000 7 994 001 0
16 000 127 976 001 0
倍数(n 每翻 4 倍) ×16.0、×16.0

★★★ ×16 就是 O(n²) 的签名第 20 章 P5019 那条)—— 顶格 n = 10⁶ 要跳约 5 × 10¹¹ 步,一秒钟想都别想。 ⚠⚠ 而随机串那一列是精确的 0:随机 26 个字母几乎没有 border,链一节都没有 ⇒ 顺手造一组「顶格随机」跑一遍,这个坑一步都看不见。

★ 对照:正解那一句递推在 n = 16000 上只走 16 000 步 —— 差 7998 倍。 ⇒ ★★ 一句记忆化,复杂度从 O(n²) 掉到 O(n),而代码只长了一行

p3435Jump.cpp⚠ 慢法:答案全对,O(n²)

6★★★ 对拍:四个待测版本里,两个对拍永远抓不到 —— 而理由完全不同

p3435Gen.cpp★ 生成器:档 2 起造周期串,把 border 链拉长
// P3435 的生成器:./p3435Gen 种子 [档位]
//
// ★ 四个待测版本里,**只有一个是对拍抓得到的**:
// · p3435Long(用了最长 border)→ **存在某个前缀,它的 border 链长度 ≥ 2**
// (也就是「最长 border」和「最短非零 border」不是同一个);
// · p3435Int(答案用 int)→ ⚠ 要答案越过 2³¹,那条线在 **n = 65537**
// ⇒ **小数据结构上到不了** ⇒ 对拍四档全是 0;
// · p3435Jump(沿链一路跳到底)→ ⚠ **答案永远对** ⇒ 对拍原理上看不见。
// ⇒ ★★ 两种「对拍是聋的」,理由完全不同:一个是「答案永远对」,一个是「档位到不了那条线」。
//
// 档位:
// 0 ★ 顺手写的:26 个小写字母随机,n = 10~40(⚠ border 链几乎全是空的 ⇒ Long 版蒙对)
// 1 ⚠ 字母表压到 2 个(a / b)
// 2 ⚠ 周期串:一小段重复若干次 ⇒ border 链一下子变长
// 3 ★ 最终档:周期串 + 随机尾巴
//
// ⚠ 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() % 3u;
int d = 1 + (int)rd; // 一段 1~3 个字符 ⇒ border 链长
unsigned rk = rng() % 8u;
int k = 4 + (int)rk;
string unit;
for (int i = 0; i < d; i++) unit.push_back(pick());
for (int i = 0; i < k; i++) s += unit;
if (mode == 3) { // 随机尾巴
unsigned rt = rng() % 6u;
int t = (int)rt;
for (int i = 0; i < t; i++) s.push_back(pick());
}
} 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 按定义暴力:1200 轮 0 组不一致):

档位 ✗ 最长 border int ⚠ 跳到底
0 ★ 顺手写的(26 个字母随机) 1 0 0
1 ⚠ 字母表压到 2 个 262 0 0
2 ⚠ 周期串(一段重复 4~11 次) 300 0 0
3 ★ 最终档(周期串 + 随机尾巴) 300 0 0
★★★ 三条读得出来的结论
  1. ★★★ 两列全 0,而那两个 0 完全不是一回事

    为什么对拍抓不到 那要怎么抓
    ⚠ 跳到底 答案永远对 —— 它只是慢 换尺子数次数(第 ⑤ 步)
    int 档位到不了那条线 —— 要 n ≥ 65537 才溢出 一句,再拿真程序在线两侧各跑一遍(第 ④ 步)

    ⇒ ★★ 本书两类「对拍是聋的」,在同一页上并排出现: 一类是「只影响复杂度、不影响答案」, 一类是「小数据结构上碰不到」。 ⚠ 看到一列全是 0,先问是哪一类 —— 加轮数对两者都没用,而救法完全不同。

  2. ★★ 顺手写的那一档几乎抓不到唯一那个抓得到的 —— 26 个字母随机,300 轮只有 1 轮;字母表压到 2 个就是 262。 ⇒ 第 22 章 P1020 那条「生成器该照抄题面的比值」在字符串题上的老形态: 拧的不是长度,是字母表。

  3. 四格「触发 ≡ 抓获」一个不差:触发条件是「存在某个前缀的 border 链长度 ≥ 2」。

7★ 哪一版就已经能过了

★ 这道题的三关,一关在读题、一关在类型、一关在记忆化
版本 结果 说明
p3435.cpp AC 一趟 next + 一句递推,O(n)
✗ 最长 border WA 样例就死(24 → 12)
int WA 对拍四档全 0n ≥ 65537 才现形
⚠ 跳到底 TLE ⚠ 答案全对;顶格全 a 要跳 5 × 10¹¹ 步

⇒ ★★ 一句话带走:这道题最难的一步在写代码之前 —— 把题面那句「AQQ 的前缀」翻成「0 < b ≤ L/2」, 再看出「要最大周期 = 要最短非零 border」。 ⇒ 而翻错的代价不是编译错误,是一个看起来很合理的错答案 (隔壁那道题的公式,在这道题上照样跑得飞快)。