把 s 接成 s+s 后从下标 1 找 s 的首次再现,该位置就是整除 n 的最小周期 d,答案 n//d;CPython 的 find 用 Two-Way 算法线性完成,与 KMP 同阶。
OJ: roj
题目 ID: 1457
难度:普及+/提高-
标签:字符串KMP周期python
创建: 2026-09-30 11:53
更新: 2026-10-07 11:01
形式化题目
给定若干个仅含英文字母的非空字符串,对每个字符串
例如 ababab = (ab)^3,abcd 无法由更短的串重复得到,
正解
思路
朴素做法与其瓶颈。 设 s[:d] * (n // d) == s,取最小的可行
关键转化:把串看成环,用"循环移位"刻画周期。 把
- 若
, :环上每隔 格的内容完全相同,左移 位后逐位对齐, 不变。 - 若左移
位不变:从环上任一位置出发,沿环每走 步字符都相同(走 步同理)。 由裴蜀定理,存在整数 使 ,即模 意义下 走 个 步等价于走 步,所以左移 位也不变。
于是"不变移位"集合对加法封闭,构成模
整除所有不变移位;对 自己用一次上面的推理得 也是不变移位且 ,由最小性 ,即 。 - 左移
位不变说明环转 格后逐位相同,而 ,所以把环剪开恰好得到 段相同的 ,即 。 - 不存在更短的
使 ,否则 是比 更小的正不变移位。
所以答案
线性求
位置 find 一定有返回,无需判
用样例 ababab(s+s = abababababab)逐个起点检查
| 出现起点 |
从该位起取 6 个字符 | 是否等于 ababab |
|---|---|---|
| 0 | ababab |
是(平凡:自己,跳过) |
| 1 | bababa |
否 |
| 2 | ababab |
是(第一次再现) |
| 3 | bababa |
否 |
| 4 | ababab |
是 |
| 5 | bababa |
否 |
| 6 | ababab |
是(平凡:第二个 |
观察重点:再现位置恰好是 bababa ababab,与表中 aaaa 的再现位置是 abcd 除位置
这个 find 并不比 KMP 慢:经典 KMP 做法先求 fail 数组,令 str/bytes.find 底层采用 Two-Way(Crochemore–Perrin)算法,
最坏情况同样是线性 main.py 就采用 find 写法。
代码
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-09-30 11:53
# update_at: 2026-09-30 11:53
import sys
def max_repeats(s: bytes) -> int:
"""返回 s 最多由多少个相同子串重复连接而成。"""
# s 在 s+s 中首次再现的位置(从 1 起)= 能整除 len(s) 的最小周期,
# CPython 的 find 用 Two-Way 算法,最坏也是线性时间。
period = (s + s).find(s, 1)
return len(s) // period
def solve() -> None:
# 每行一个仅含字母的串,读到 "." 结束;按空白切分即可逐串处理
for s in sys.stdin.buffer.read().split():
if s == b".":
break
print(max_repeats(s))
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-09-30 12:00
* update_at: 2026-10-05 02:26
*/
// KMP 求 fail 数组:fail[n-1] 是整个串的最长公共前后缀长度,
// 则 p = n - fail[n-1] 是最小周期;p 整除 n 时答案是 n/p,否则串不是幂串,答案 1。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000005;
typedef long long ll;
int len; // 当前串长
char s[MAXN]; // 当前字符串,下标从 0 开始
int fail[MAXN]; // fail[i] = s[0..i] 的最长相等前后缀长度(不含整个串)
// 对当前串求 fail 数组(KMP 失配函数)。
void get_fail() {
fail[0] = 0;
// j 表示 s[0..i-1] 匹配到的前缀长度,尝试把 s[i] 接在 s[j] 后面
for (int i = 1, j = 0; i < len; i++) {
while (j > 0 && s[i] != s[j]) j = fail[j - 1]; // 失配则跳到更短的前后缀
if (s[i] == s[j]) j++;
fail[i] = j;
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
while (cin >> s) {
len = strlen(s); // 声明 len 为 int 即可,无强制转换
if (len == 1 && s[0] == '.') break; // 句号结束输入
get_fail();
int p = len - fail[len - 1]; // 最小周期
if (len % p == 0) {
cout << len / p << "\n"; // 整周期,串是幂串
} else {
cout << 1 << "\n"; // 最小周期不整除 n,无法由重复子串构成
}
}
return 0;
}正文与代码的对应关系:
max_repeats(s)中的period = (s+s).find(s, 1)就是上文的最小整除周期; len(s) // period即答案( 由子群性质保证,整除必成立); solve()里sys.stdin.buffer.read().split()把输入按空白切成一个个串, 读到b"."提前结束,与"每行一个串、.停止"的格式等价(串只含字母)。
复杂度
设一次输入的所有串长总和为
- 时间:每串拼
与一次 find都是,总计 ,与 KMP 正解同阶。 本地真实数据 10 组全部通过,最慢单测约 0.04s(OJ 时限 1s),无超时风险。 - 空间:单串峰值
( s+s长, 时约 3MB,远低于 128MB 限制)。
总结
- 幂串问题
的核心是求最小整除周期 ,答案 。 - 把串看成环后,“
是幂串"等价于"存在非平凡循环移位使 不变”;由裴蜀定理, 不变移位构成子群,最小正元自动整除 ——这一步把"逐个验证约数"变成了"找第一个再现位置"。 在 中(位置 )第一次再现的位置就是 ,任何线性字符串匹配 (KMP、Z 函数、Two-Way find)都能完成这最后一步。- Python 实现要点:
bytes读入免解码,内建find以 C 速度做线性匹配, 全程没有 Python 层的逐字符循环。