KMP 前缀函数一趟线性求出每个前缀的最长 border,最短周期即 i−π[i];π[i]>0 且 i−π[i] 整除 i 时即由该循环节整分,输出 (i, i/(i−π[i]))。
OJ: roj
题目 ID: 3040
难度:普及+/提高-
标签:字符串KMP前缀函数周期python
创建: 2026-10-01 11:40
更新: 2026-10-07 13:50
形式化题目
给定长度
若可以,求出其中最大的
正解
思路
先看朴素做法的瓶颈。 对每个
把「循环节」翻译成「border」。 长度
两者一一对应且方向相反,所以最短周期由最长 border 决定:
问题从「对每个前缀找周期」变成「对每个前缀求最长 border」。
一趟线性求出所有 while 每次回跳都让它严格
减小,总回跳次数不超过总增加次数,所以整趟扫描是
从
是必要条件。设 ( ),则 也是周期, 由 最小得 。又 且 说明 ,于是 。此时用周期引理( 都是长度 串的周期且 时 也是周期): 是周期,而它 ,由 的 最小性只能是 ,即 ;再由 传递得到 。 也是充分条件:把 按 切成 段, 由周期性每一段都等于首段,于是它就是首段的重复, 。
为什么取
第一条管「
用样例 aabaabaabaab 走一遍。 下表列出每个前缀的
| 前缀 | 输出 | ||||
|---|---|---|---|---|---|
| 1 | a |
0 | 1 | 是 | — (题面只统计 |
| 2 | aa |
1 | 1 | 是 | 2 2 |
| 3 | aab |
0 | 3 | 是 | — ( |
| 4 | aaba |
1 | 3 | 否 | — |
| 5 | aabaa |
2 | 3 | 否 | — |
| 6 | aabaab |
3 | 3 | 是 | 6 2 |
| 7 | aabaaba |
4 | 3 | 否 | — |
| 8 | aabaabaa |
5 | 3 | 否 | — |
| 9 | aabaabaab |
6 | 3 | 是 | 9 3 |
| 10 | aabaabaaba |
7 | 3 | 否 | — |
| 11 | aabaabaabaa |
8 | 3 | 否 | — |
| 12 | aabaabaabaab |
9 | 3 | 是 | 12 4 |
观察 aaba、aabaa 都不是 aab 的整数倍
重复。输出与样例的 2 2 / 6 2 / 9 3 / 12 4 完全一致。
代码
prefix_function 就是上面那趟线性建表,j 是失配链游标;repeats 把「最短周期 +
两个输出条件」写成生成器 + 推导式:period < length 负责排除 length % period == 0 负责整除,二者分别对应上面那两条判据;solve 只做读入、
按组调用与拼接输出。
Python 版:
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-01 11:41
# update_at: 2026-10-01 11:50
import sys
def prefix_function(s: bytes) -> list[int]:
"""KMP 失配数组:nxt[i] 是 s[:i+1] 最长的「真前缀 = 真后缀」的长度,下标从 0 起。"""
n = len(s)
nxt = [0] * n
for i in range(1, n):
j = nxt[i - 1]
while j and s[i] != s[j]: # 失配就沿失配链回跳,直到能接上或跳到 0
j = nxt[j - 1]
if s[i] == s[j]:
j += 1
nxt[i] = j
return nxt
def repeats(s: bytes) -> list[tuple[int, int]]:
"""每个「恰好由循环节拼成的前缀」(前缀长度 length, 重复次数 k),按 length 升序。"""
nxt = prefix_function(s)
# 长度 length 的最短周期 period = length - nxt[length-1];
# period < length 说明循环节不是前缀本身,即 k > 1;
# period 再整除 length 时,该前缀恰好是 k = length/period 个 period 的重复。
cycles = ((length, length - nxt[length - 1]) for length in range(2, len(s) + 1))
return [
(length, length // period)
for length, period in cycles
if period < length and length % period == 0
]
def solve() -> None:
data = iter(sys.stdin.buffer.read().split()) # token 混有字符串 s,不能整体 map(int)
out: list[str] = []
case = 0
while n := int(next(data)): # 每组先读长度 n,读到 0 结束
case += 1
s = next(data)
out.append(f"Test case #{case}")
out += [f"{length} {k}" for length, k in repeats(s)] # 输入保证 len(s) == n,无需再截断
out.append("") # 每组末尾的空行,同时也是下一组的前导空行
print('\n'.join(out))
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-06 15:31
* update_at: 2026-10-06 15:31
*/
// main.cpp:KMP 前缀函数求每个前缀的最短周期。
// 长度为 i 的前缀,最短周期 period = i - nxt[i](nxt[i] 是最长 border 长度);
// 当 nxt[i] > 0(保证 K > 1)且 period 整除 i 时,前缀恰好由 K = i / period 个循环节拼成。
#include <cstdio>
#include <cstring>
typedef long long ll;
const int MAXN = 1000005;
int n; // 当前测试数据的字符串长度
char s[MAXN]; // 字符串,下标从 1 开始
int nxt[MAXN]; // nxt[i]:s[1..i] 的最长「真前缀 = 真后缀」长度(border)
// 一趟线性求出整个 nxt 数组:从 nxt[i-1] 沿失配链回跳,看能否接上 s[i]。
void get_next() {
nxt[1] = 0;
int j = 0; // j 是当前尝试的 border 长度,也是失配链游标
for (int i = 2; i <= n; i++) {
while (j && s[i] != s[j + 1]) j = nxt[j]; // 失配就回跳到更短的 border
if (s[i] == s[j + 1]) j++;
nxt[i] = j;
}
}
int main() {
int case_id = 0;
while (scanf("%d", &n) == 1 && n != 0) {
scanf("%s", s + 1);
get_next();
printf("Test case #%d\n", ++case_id);
for (int i = 2; i <= n; i++) {
int period = i - nxt[i]; // 最短周期 = 长度 - 最长 border 长度
// nxt[i] > 0 保证 K > 1;period 整除 i 保证前缀能被循环节整分
if (nxt[i] > 0 && i % period == 0)
printf("%d %d\n", i, i / period);
}
printf("\n"); // 每组数据末尾输出一个空行
}
return 0;
}复杂度
- 时间复杂度:总长
的一组数据,建失配数组与扫描输出各 ,输出本身的字符量 为 (每行含 位的长度和 )。多组数据为 ,与标准 C++ 正解同阶。 - 空间复杂度:失配数组
个整数,另加读入缓冲与输出行,合计 。 实测最大一组 峰值约 73 MB,在 128 MB 之内。
总结
本题的落点是「用前缀函数把周期问题变成查表」:
- 朴素做法对每个前缀重新比较,
;瓶颈在于前缀之间的信息没有复用; - 关键等价:
有周期 有长度 的 border,于是最短周期 = ; 只需从 沿失配链回跳即可求出,一趟扫描 ; - 输出条件收敛为「
且 整除 」,前者管 ,后者管能否整分。
真实数据 data/a.in(共 7 组,data/a.out 逐字节一致,整份数据
跑完约 0.27 s、峰值约 73 MB,远低于 1000 ms / 128 MB。
图示解析
这张图串起从题面到代码的主线:
前缀 S[1..i],问是否 = A^K (K>1) 且 K 最大
`- 枚举 p | i 逐字符比较 -> O(N^2),瓶颈是“每个前缀都从零重算”
`- 关键等价:周期 p <-> border 长度 i - p
`- 于是 最短周期 p_min = i - π(i) (π = 最长 border 长度)
|- π(i) 由 π(i-1) 沿失配链回跳得到 -> 一趟 O(N)
|- 整除判据:p_min | i <=> 字符串能被整分
`- K>1 判据:π(i) > 0 <=> p_min < i
`- 输出 (i, i / p_min),下标升序天然有序逐个节点看:第一行是朴素做法,第二行把它压成「求 border」,第三行给出唯一的判定量
下面用样例第一组 aaa 说明 nxt[i] 存的是前缀 s[0..i](0 起)的最长 border 长度,与题面的 1 起前缀长度差 1。
代码下标 i |
前缀 | nxt[i] |
题面长度 length = i + 1 |
period = length - nxt[i] |
length % period |
输出 |
|---|---|---|---|---|---|---|
| 0 | a |
0 | 1 | 1 | 0 | — (length 从 2 起) |
| 1 | aa |
1 | 2 | 1 | 0 | 2 2 |
| 2 | aaa |
2 | 3 | 1 | 0 | 3 3 |
nxt 一路增长到 n-1,是因为每个更长的前缀都能把前一个 border 接上同一个字符,
于是 period 恒为 1、K 恒等于长度——全同字符串就是这样被一路输出的。