利用 KMP 的 border 性质与失配指针链,将求最大周期转化为求每个前缀的最短正 border,通过路径压缩在 O(n) 时间内完成前缀周期求和。
OJ: roj
题目 ID: 1468
难度:普及+/提高-
标签:字符串KMP周期
创建: 2026-09-30 12:30
更新: 2026-10-07 11:01
形式化题目
给定一个长度为
对于
- 若存在一个真前缀
(即 且 非空),使得 是字符串 的前缀,则称 是 的一个周期。 的最大周期定义为 的所有周期中最长的那一个;若 不存在任何周期,则其最大周期为空串(长度为 )。
求字符串
正解
思路
1. 周期与 Border 的对偶转化
设当前考虑的前缀为
根据定义:
是 的真前缀,因此 。 是 的前缀。因为 ,字符串 的长度为 。 的前 个字符构成了 ,而后 个字符同样是 。 作为 的前缀,其长度为 。因此 的后缀(从位置 到 ,长度为 )必然等于 的前缀(长度为 )。 - 又因为
本身是 的前缀,所以 的前 个字符也就是 的前 个字符。 - 这意味着:
拥有一个长度为 的公共真前缀与真后缀(即 Border)。
反过来,如果
- 由于
,有 ,即 是 的真前缀; - 由于
,有 ; - 此时
的前 个字符刚好与 匹配,即 是 的前缀。
因此,
2. 最大周期与最短 Border
题目要求求出
- 如果
不存在任何正长度的 Border,则 没有任何周期,最大周期长度计为 。 - 如果
存在 Border,则 的最大周期长度等于 减去 的最短正 Border 长度。
3. 利用 KMP失配树与路径压缩
在 KMP 算法中,前缀函数(next 数组)记录的是每个前缀的最长非平凡 Border 长度。
根据 Border 的传递性,前缀
若每次暴力沿着
我们可以采用路径压缩(记忆化递推):
记
- 若
,说明前缀 没有 Border, 。 - 若
: - 如果子前缀
自身已有最短正 Border(即 ),则前缀 直接继承它: ; - 否则说明
自身就是最底层的非平凡 Border,直接取 。
- 如果子前缀
这样每个状态只需
代码
Python 版:
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-07-05 21:47
# update_at: 2026-07-05 21:47
import sys
def compute_next(s: str, n: int) -> list[int]:
"""计算 KMP 的 next 数组(nxt[i] 表示前缀 s[:i] 的最长非平凡 border 长度)。"""
nxt = [0] * (n + 1)
j = 0
for i in range(1, n):
while j > 0 and s[i] != s[j]:
j = nxt[j]
if s[i] == s[j]:
j += 1
nxt[i + 1] = j
return nxt
def solve() -> None:
data = iter(sys.stdin.buffer.read().split())
n = int(next(data))
s = next(data).decode()
nxt = compute_next(s, n)
# min_border[i] 记录前缀 s[:i] 的最短正 border 长度;
# 路径压缩记忆化:若其 next 指向的子前缀已有最小 border 则直接继承,否则为其自身。
min_border = [0] * (n + 1)
total_period = 0
for i in range(1, n + 1):
if nxt[i] > 0:
min_border[i] = min_border[nxt[i]] if min_border[nxt[i]] else nxt[i]
total_period += i - min_border[i]
print(total_period)
if __name__ == "__main__":
solve()C++ 版(同一算法):
cpp
/**
* 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-05 02:52
* update_at: 2026-10-05 02:52
*/
// KMP 求所有前缀的最大周期长度之和:
// 前缀 A(长度 i) 的周期 Q(长度 q) <=> A 有长度 b = i - q 的正 border,
// 所以最大周期 = i - 最短正 border。沿 next 失配链 + 路径压缩 O(n) 求出。
#include <cstdio>
#include <cstring>
typedef long long ll;
const int MAXN = 1000005;
int n; // 串长
char s[MAXN]; // 输入串,下标从 1 开始
int nxt[MAXN]; // KMP 失配数组:nxt[i] = 前缀 s[1..i] 的最长非平凡 border 长度
int min_border[MAXN]; // min_border[i] = 前缀 s[1..i] 的最短正 border 长度(0 表示没有)
int main() {
scanf("%d", &n);
scanf("%s", s + 1);
// 求 next 数组
nxt[1] = 0;
int j = 0;
for (int i = 2; i <= n; i++) {
while (j > 0 && s[i] != s[j + 1]) j = nxt[j];
if (s[i] == s[j + 1]) j++;
nxt[i] = j;
}
// 路径压缩求每个前缀的最短正 border:
// 若 nxt[i] 自己已有更短的 border 就继承,否则 nxt[i] 就是最短的
ll ans = 0;
for (int i = 1; i <= n; i++) {
if (nxt[i] == 0) {
min_border[i] = 0; // 没有任何 border,最大周期为空串
} else {
if (min_border[nxt[i]] > 0)
min_border[i] = min_border[nxt[i]];
else
min_border[i] = nxt[i];
ans += i - min_border[i]; // 最大周期长度 = i - 最短正 border
}
}
printf("%lld\n", ans);
return 0;
}复杂度
- 时间复杂度:
。预处理 KMP 的 next数组耗时;随后进行一次线性扫描,通过已计算的 min_border数组转移并累加答案,总耗时为 。 - 空间复杂度:
。需要存储原字符串以及长度为 的 next数组与min_border数组。
总结
本题的关键在于通过数学观察将「求最大周期