0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P2375,日期见页头。两边不一致时信原站。
题目描述
园长给动物们讲解 KMP 算法。
熊猫:「对于字符串
S的前i个字符构成的子串,既是它的后缀又是它的前缀的字符串中(它本身除外), 最长的长度记作next[i]。」例:
S为abcababc,则next[5] = 2。同理next[1] = next[2] = next[3] = 0,next[4] = next[6] = 1,next[7] = 2,next[8] = 3。
下课前,园长提出了一个问题:
「我现在希望求出一个更强大的
num数组 —— 对于字符串S的前i个字符构成的子串, 既是它的后缀同时又是它的前缀,并且该后缀与该前缀不重叠,将这种字符串的数量记作num[i]。例如
S为aaaaa,则num[4] = 2。这是因为S的前 4 个字符为aaaa,其中a和aa都满足性质『既是后缀又是前缀』,同时保证这个后缀与这个前缀不重叠。 而aaa虽然满足性质『既是后缀又是前缀』,但遗憾的是这个后缀与这个前缀重叠了,所以不能计算在内。 同理,num[1] = 0,num[2] = num[3] = 1,num[5] = 2。」
特别地,为了避免大量的输出,你不需要输出 num[i] 分别是多少,
你只需要输出所有 (num[i]+1) 的乘积,对 10⁹ + 7 取模的结果即可。
输入格式
第 1 行仅包含一个正整数 n,表示测试数据的组数。随后 n 行,每行一个字符串 S(仅含小写字母)。
输出格式
包含 n 行,每行一个整数,表示这组测试数据的答案对 10⁹+7 取模的结果。
数据范围
| 测试点 | 约定 | 测试点 | 约定 | |
|---|---|---|---|---|
| 1 | n ≤ 5, L ≤ 50 |
6 | n ≤ 5, L ≤ 100,000 |
|
| 2、3 | n ≤ 5, L ≤ 200 |
7 | n ≤ 5, L ≤ 200,000 |
|
| 4、5 | n ≤ 5, L ≤ 10,000 |
8 | n ≤ 5, L ≤ 500,000 |
|
| 9、10 | n ≤ 5, L ≤ 1,000,000 |
时限 1 秒,内存 524288 KB(512 MB)。
输入输出样例
输入
3 aaaaa ab abcababc
输出
36 1 32
第一组 aaaaa:num 是 0 1 1 2 2 ⇒ 乘积 1×2×2×3×3 = 36。
★ 注意第二组 ab 的答案是 1 —— 它一个 border 都没有,(0+1)×(0+1) = 1。
1★★ 先把题面翻译成一句话:num[i] = 长度 ≤ i/2 的非空 border 个数
一个既是前缀又是后缀的串,长度 b,放在长度 L 的串里:
前缀占 [0, b)、后缀占 [L-b, L)。⇒ 两段不重叠 ⟺ b ≤ L − b ⟺ b ≤ L/2。
⇒ num[i] = s[0..i-1] 里长度 ≤ i/2 的非空 border 有几个。
题面自己给的例子正好对上:aaaa 的 border 是 1、2、3,其中 ≤ 2 的有两个 ⇒ num[4] = 2。
① border 链的长度能递推。 设 cnt[i] = s[0..i] 的非空 border 个数:
cnt[i] = nxt[i] ? cnt[nxt[i]-1] + 1 : 0;—— 因为「border 的 border 还是 border」,一个前缀的所有 border 正好排成一条链,
而这条链的下一节就是 nxt[i] 那个前缀的链。
(★ 第 48 章第 5 步说 nxt 是「拿 p 去匹配 p」;这里再进一步:
整个 nxt 数组其实是一棵树,第 i 个点的父亲是 nxt[i]。)
② 「长度 ≤ 一半」那个限制,再养一个指针就够。
让 k 像 KMP 一样跟着 i 往前走,走完再缩回来:
while (k > 0 && s[i] != s[k]) k = nxt[k-1];
if (s[i] == s[k]) k++;
while (k > 0 && 2*k > i+1) k = nxt[k-1]; // ★ 缩到不重叠为止
num = k ? cnt[k-1] + 1 : 0;⇒ k 每轮最多 +1、每次回退至少 −1 且不为负 ⇒ 回退总数均摊 O(L),整体一趟 O(L)。
⚠ 而所有人的第一版都是「每个 i 从 nxt[i] 重新往下跳」—— 那是 O(L²),第 ④ 步量它。
// P2375 [NOI2014] 动物园 —— 正解:两个指针,一趟 O(L)//// ★★ 题面要的 num[i]:s 的前 i 个字符里,「既是前缀又是后缀、**而且两段不重叠**」的串有几个。// 翻译过来就是:**长度 ≤ i/2 的非空 border 有几个。**//// ★ 拆成两半,每一半都只要一句话:// ① **border 链的长度**能递推:cnt[i] = s[0..i] 的非空 border 个数// = cnt[nxt[i]-1] + 1(nxt[i] > 0 时),否则 0。// —— 因为「border 的 border 还是 border」,整条链就是一棵树上的一条路径。// ② **「长度 ≤ 一半」那个限制**只要再养一个指针 k:它像 KMP 一样跟着 i 往前走,// 走完再 `while (2k > i+1) k = nxt[k-1]` 缩回来。// ⇒ k 只增不减地往前走、每次回退至少减一 ⇒ **两个指针都是均摊 O(1)**,整体 O(L)。// 最后 num[i] = k > 0 ? cnt[k-1] + 1 : 0(长度 ≤ k 的 border 就是「k 自己 + k 那个前缀的全部 border」)。//// ⚠ 三个容易漏的角:// · 「不重叠」那半漏掉 ⇒ 变成 cnt[i](见 p2375Overlap.cpp);// · 那个 k 不用均摊指针、每个 i 都从头跳 ⇒ O(L²),**答案对但跑不完**(见 p2375Chain.cpp);// · **多测**:答案累乘的变量每组都要重置(见 p2375Ans.cpp)。#include <cstdio>#include <cstring>using namespace std;
static const int MAXN = 1000006;static const long long MOD = 1000000007LL;static char s[MAXN];static int nxt[MAXN], cnt[MAXN];
int main() { int T; if (scanf("%d", &T) != 1) return 0; while (T--) { if (scanf("%s", s) != 1) return 0; int n = (int)strlen(s); nxt[0] = 0; cnt[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; cnt[i] = j ? cnt[j - 1] + 1 : 0; } long long ans = 1; // ⚠ 每组都要重置 for (int i = 1, k = 0; i < n; i++) { while (k > 0 && s[i] != s[k]) k = nxt[k - 1]; if (s[i] == s[k]) k++; while (k > 0 && 2 * k > i + 1) k = nxt[k - 1]; // ★ 缩到「不重叠」为止 long long num = k ? cnt[k - 1] + 1 : 0; ans = ans * ((num + 1) % MOD) % MOD; } printf("%lld\n", ans); } return 0;}点「运行 ▶」看结果
参照物不碰 next、不提 border 链:对每个前缀,把 b = 1..L/2 挨个试
「前 b 个字符 == 后 b 个字符吗」。⇒ 四档 1200 轮,和正解 0 组不一致。
★ 它验的是「不重叠 ⟺ b ≤ L/2」和「num[i] = cnt[k-1]+1」这两步翻译。
2⚠ 错法一:漏掉了「不重叠」
题面把「不重叠」说了两遍,还专门举了反例(aaaa 里的 aaa 不算)。
漏掉这半句,num[i] 就变成了「所有非空 border 的个数」= cnt[i]。
★ 触发条件:存在 i 使 2 × nxt[i] > i+1 —— 也就是某个前缀「自己叠在自己身上」。
⇒ 官方样例第一组(aaaaa)一测就死:36 → 120。
⚠ 而顺手写的随机数据几乎碰不到它(第 ⑤ 步:26 个字母随机,3000 轮才抓到 5 次)。
// P2375 ✗ 错法一:漏掉了「这个后缀与这个前缀**不重叠**」//// ⚠ 题面把这句话说了两遍,还专门举了例子:`S = aaaaa` 时 `num[4] = 2`,// 因为 `aaa` 虽然既是前缀又是后缀,**但两段重叠了**,不算。// ⇒ 漏掉这半句,num[i] 就变成了「**所有**非空 border 的个数」= cnt[i]。// ★ 触发条件:**存在某个前缀,它有长度 > 一半的 border**// —— 也就是那个前缀「自己叠在自己身上」。⇒ 官方样例第一组(`aaaaa`)一测就死。#include <cstdio>#include <cstring>using namespace std;
static const int MAXN = 1000006;static const long long MOD = 1000000007LL;static char s[MAXN];static int nxt[MAXN], cnt[MAXN];
int main() { int T; if (scanf("%d", &T) != 1) return 0; while (T--) { if (scanf("%s", s) != 1) return 0; int n = (int)strlen(s); nxt[0] = 0; cnt[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; cnt[i] = j ? cnt[j - 1] + 1 : 0; } long long ans = 1; for (int i = 1; i < n; i++) { long long num = cnt[i]; // ⚠ 所有 border,没管重不重叠 ans = ans * ((num + 1) % MOD) % MOD; } printf("%lld\n", ans); } return 0;}点「运行 ▶」看结果
3⚠ 错法二:多测,可答案那个变量忘了每组重置
题面第一行是组数 n(n ≤ 5),而答案是累乘出来的。
把 long long ans = 1; 写在 while (T--) 外面,第二组就带着第一组的积开始乘。
★ 触发条件:组数 ≥ 2,而且前面某一组的答案不是 1。 ⇒ 官方样例三组(36 / 1 / 32)一测就死:它打出 36 / 36 / 1152。
4★★★ 慢法:那个指针每轮从头跳 —— 答案永远对,只能数次数
// P2375 解析页上那几个「对拍看不见」的数字的出处。// ./p2375Count 人话版// ./p2375Count csv 给 check:viz 用//// ★★ 这道题唯一一个对拍抓不到的错法是「那个 k 每轮都从头跳」——它**答案永远对**,// ⇒ 只能[换尺子数次数](/ch/48-kmp/)。这里数的是「k 一共回退了多少步」:// · 正解:k 只增不减地往前走,回退总数**均摊 O(L)**;// · 跳链版:每个 i 都从 nxt[i] 重新往下跳 ⇒ 全是同一个字母时是 **O(L²)**。// ⚠ 而随机串上两者几乎一样 —— **顺手造一组顶格随机跑一遍,这个坑一步都看不见。**#include <bits/stdc++.h>using namespace std;
static void build(const string& s, vector<int>& nxt) { int n = (int)s.size(); nxt.assign(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; }}/** 正解那个均摊指针:一共回退多少步 */static long long stepsAmortized(const string& s) { int n = (int)s.size(); vector<int> nxt; build(s, nxt); long long steps = 0; for (int i = 1, k = 0; i < n; i++) { while (k > 0 && s[i] != s[k]) { k = nxt[k - 1]; steps++; } if (s[i] == s[k]) k++; while (k > 0 && 2 * k > i + 1) { k = nxt[k - 1]; steps++; } } return steps;}/** 跳链版:每个 i 都从 nxt[i] 重新往下跳 */static long long stepsRestart(const string& s) { int n = (int)s.size(); vector<int> nxt; build(s, nxt); long long steps = 0; for (int i = 1; i < n; i++) { int k = nxt[i]; while (k > 0 && 2 * k > i + 1) { k = nxt[k - 1]; steps++; } } return steps;}
int main(int argc, char** argv) { bool csv = argc > 1 && string(argv[1]) == "csv"; mt19937 rng(20260908u); vector<int> NS = {1000, 4000, 16000}; vector<long long> amoA, resA, resR; for (int n : NS) { string a((size_t)n, 'a'); amoA.push_back(stepsAmortized(a)); resA.push_back(stepsRestart(a)); string r; for (int i = 0; i < n; i++) { unsigned c = rng() % 26u; r.push_back((char)('a' + c)); } resR.push_back(stepsRestart(r)); } /* 顶格输入有多大:5 组 × 10⁶ 个字符 */ long long inBytes = 5LL * 1000000 + 5 + 2;
if (csv) { for (size_t i = 0; i < NS.size(); i++) printf("amoA%d,%lld\n", NS[(int)i], amoA[i]); for (size_t i = 0; i < NS.size(); i++) printf("resA%d,%lld\n", NS[(int)i], resA[i]); for (size_t i = 0; i < NS.size(); i++) printf("resR%d,%lld\n", NS[(int)i], resR[i]); printf("ratioRes1,%.1f\nratioRes2,%.1f\n", (double)resA[1] / (double)resA[0], (double)resA[2] / (double)resA[1]); printf("ratioAmo1,%.1f\nratioAmo2,%.1f\n", (double)amoA[1] / (double)amoA[0], (double)amoA[2] / (double)amoA[1]); printf("inBytes,%lld\n", inBytes); return 0; } printf("「k 一共回退了多少步」(全是 a 的串 / 随机 26 个字母):\n"); printf(" %8s %14s %14s %14s\n", "n", "正解(均摊)", "跳链版(全a)", "跳链版(随机)"); for (size_t i = 0; i < NS.size(); i++) printf(" %8d %14lld %14lld %14lld\n", NS[(int)i], amoA[i], resA[i], resR[i]); printf(" n 每翻 4 倍:正解 ×%.1f ×%.1f(线性);跳链版 ×%.1f ×%.1f(★ O(n²) 的签名)\n", (double)amoA[1] / (double)amoA[0], (double)amoA[2] / (double)amoA[1], (double)resA[1] / (double)resA[0], (double)resA[2] / (double)resA[1]); printf("⇒ 顶格 L = 10⁶ 时跳链版约 2.5 × 10¹¹ 步;而顶格输入本身有 %lld 字节(5 组 × 10⁶)\n", inBytes); return 0;}点「运行 ▶」看结果
n(全是 a) |
★ 正解(均摊指针) | ⚠ 每轮从头跳 | 随机 26 个字母(从头跳) |
|---|---|---|---|
| 1 000 | 499 | 249 500 | ★ 0 |
| 4 000 | 1 999 | 3 998 000 | ★ 0 |
| 16 000 | 7 999 | 63 992 000 | ★ 0 |
| 倍数(n 每翻 4 倍) | ×4.0(线性) | ★ ×16.0(O(n²) 的签名) |
— |
★ n = 16000 上两者差 8000 倍;顶格 L = 10⁶ 时「从头跳」约 2.5 × 10¹¹ 步。
秒表(A 机:WSL2 6.18 / 8 核 / 8 GB,2026-09-08,独占;单组全 a):
L |
正解 | ⚠ 每轮从头跳 |
|---|---|---|
| 10⁴ | < 0.01 秒 | 0.04 秒 |
| 10⁵ | < 0.01 秒 | ★ 4.37 秒(时限 1 秒) |
⇒ ★★ 那堵墙正好落在测试点 5 和 6 之间(L ≤ 10⁴ ↔ L ≤ 10⁵)
⇒ 「每轮从头跳」那一版稳拿 50 分(测试点 1~5)。
⚠⚠ 而随机串那一列是精确的 0:随机 26 个字母几乎没有 border
⇒ 顺手造一组顶格随机跑一遍,这个坑一步都看不见。
5★★★ 对拍:顺手写的那一档同时打出两个 0,而两个 0 一个救得回来、一个救不回来
// P2375 的生成器:./p2375Gen 种子 [档位]//// ★ 三个待测版本各靠什么现形:// · p2375Overlap(漏掉「不重叠」)→ **存在某个前缀,它的最长 border 超过了一半**// (也就是 `2 × nxt[i] > i+1`);// · p2375Ans(答案没每组重置) → **组数 ≥ 2,而且前面某组的答案不是 1**;// · p2375Chain(k 每轮从头跳) → ⚠ **答案永远对**,对拍原理上看不见。//// 档位:// 0 ★ 顺手写的:**一组**,26 个小写字母随机,L = 10~40// ⚠ 这一档同时把两个错法打成 0:随机串几乎没有 border(Overlap 抓不到),// 而且**只有一组**(Ans 结构上不可能现形)// 1 ⚠ 字母表压到 2 个// 2 ⚠ 周期串:一小段重复若干次 ⇒ 长 border 满地都是// 3 ★ 最终档:**2~5 组** + 周期串//// ⚠ 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); };
int T = 1; if (mode == 3) { unsigned rt = rng() % 4u; T = 2 + (int)rt; } printf("%d\n", T); for (int t = 0; t < T; t++) { string s; if (mode >= 2) { unsigned rd = rng() % 3u; int d = 1 + (int)rd; unsigned rk = rng() % 8u; int k = 3 + (int)rk; string unit; for (int i = 0; i < d; i++) unit.push_back(pick()); for (int i = 0; i < k; i++) s += unit; } else { unsigned r = rng() % 31u; int n = 10 + (int)r; for (int i = 0; i < n; i++) s.push_back(pick()); } printf("%s\n", s.c_str()); } return 0;}点「运行 ▶」看结果
四档 × 300 轮(正解 vs 按定义暴力:1200 轮 0 组不一致):
| 档位 | ✗ 漏掉不重叠 | ✗ ans 没重置 | ⚠ 每轮从头跳 |
|---|---|---|---|
| 0 ★ 顺手写的(一组,26 个字母随机) | ★ 0 | ★ 0 | ★ 0 |
| 1 ⚠ 字母表压到 2 个 | 139 | ★ 0 | ★ 0 |
| 2 ⚠ 周期串(一段重复 3~10 次) | 300 | ★ 0 | ★ 0 |
| 3 ★ 最终档(2~5 组 + 周期串) | 300 | 300 | ★ 0 |
-
★★★ 档 0 那一行有三个 0,而三个 0 是三件不同的事:
那个 0 是什么 怎么分辨 / 怎么救 ✗ 漏掉不重叠 概率低 ★ 把轮数加深到 3000 轮 ⇒ 抓到 5 次;真正的救法是换档位(字母表压到 2 ⇒ 139) ✗ ans 没重置 结构性 那一档只有一组数据 ⇒ 加到三万轮也不会现形,只能改生成器 ⚠ 每轮从头跳 答案永远对 对拍原理上看不见 ⇒ 换尺子数次数(第 ④ 步) ⇒ ★★ 第 23 章 P1049 那条「两种 0,加深轮数就能分开」在这一页凑齐了三种。 ⚠ 看到一整行 0,先一个一个问它是哪一种 —— 三种的救法完全不同。
-
★★ 「多测」这件事本身就是一个档位 —— 前三档都只造一组数据, 于是「ans 没重置」那一列结构性地全是 0。 ⇒ 题面第一行写着组数,生成器就必须真的造多组(顺手写的那种「反正只测一组」会漏掉一整类 bug)。
-
★ 八格「触发 ≡ 抓获」一个不差:
2 × nxt[i] > i+1/「组数 ≥ 2 且前面某组答案 ≠ 1」。
6★ 官方那三组样例:把两个「答案错」的都打死了
| ✗ 漏掉不重叠 | ✗ ans 没重置 | ⚠ 每轮从头跳 | |
|---|---|---|---|
| 官方样例(三组) | ★ 死(36 → 120) | ★ 死(36 / 36 / 1152) | 放过 |
★ 出题人给的三组一组比一组会问:aaaaa 专问「不重叠」、ab 专问「一个 border 都没有」、
abcababc 是正常的一组 —— ⚠ 而三组放在一起才问得出「ans 有没有重置」。
⇒ ★ 这和第 47 章 P1071 那次正好相反:那道题的三组样例分别对应三种终止状态,
却仍然放过了最深的那个错法;这道题的三组是全打死。
⚠ 而它们对「每轮从头跳」完全无能为力 —— 那一版的答案是对的 (第 20 章 P5019 那条:样例这个过滤器筛的是「答案错」)。
7★ 哪一版就已经能过了
| 版本 | 结果 | 说明 |
|---|---|---|
★ p2375.cpp |
AC | 两个指针一趟 O(L);顶格输入 5 MB(5 组 × 10⁶) |
⚠ p2375Chain.cpp |
50 分 | 测试点 1~5(L ≤ 10⁴,0.04 秒);L = 10⁵ 就要 4.37 秒 |
| ✗ 漏掉不重叠 | WA | 样例就死;⚠ 顺手写的对拍 3000 轮才抓 5 次 |
| ✗ ans 没重置 | WA | 样例就死;⚠ 只造一组数据的对拍结构上抓不到 |
⇒ ★★ 一句话带走:这道题的「难」全在第一步翻译上 ——
把「不重叠」读成「长度 ≤ 一半」,把「border 的 border 还是 border」用成一条递推。
⇒ 而剩下两个坑(多测重置、均摊指针)和 NOI 没有关系,
它们在任何一道多测的题、任何一处 while 回退里都会再出现一次。