[POI 2006] OKR-Periods of Words
周期和 border 是同一枚硬币的两面:最长 period = len - 最短 border,沿前缀函数链递推。
OJ: luogu
题目 ID: P3435
难度:提高+/省选-
标签:KMP周期递推
日期: 2026-07-16 19:57
题意
对字符串
周期定义:
样例
思路
一句话本质:周期和 border 是同一枚硬币的两面——长度为
直接验证一个周期要做什么?
先看暴力:对每个前缀枚举候选周期
// brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
#include <bits/stdc++.h>
using namespace std;
int n;
string s;
// 判断前缀 s[1..len] 的前缀 s[1..period_len] 是否是一个合法 period。
bool is_period(int len, int period_len) {
if (period_len <= 0 || period_len >= len) {
return false;
}
for (int i = 1; i <= len; i++) {
int pos = (i - 1) % period_len + 1;
if (s[i] != s[pos]) {
return false;
}
}
return true;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
cin >> s;
s = " " + s;
long long ans = 0;
for (int len = 1; len <= n; len++) {
for (int period_len = len - 1; period_len >= 1; period_len--) {
if (is_period(len, period_len)) {
ans += period_len;
break;
}
}
}
cout << ans << '\n';
return 0;
}每验证一个
能不能把"逐位重扫"变成"一次判断"?
周期
所以"
那么最长周期对应什么?
KMP 只给了最长 border,最短的怎么找?
前缀函数
每条链都跳到底会不会太慢?
会。像
其中
失配树视角
这个递推从失配树上读最清楚。失配树:节点
flowchart BT
classDef root fill:#ffd,stroke:#888,stroke-width:2px
2["2: bab"] --> 0["0: b"]
4["4: babab"] --> 2["2: bab"]
6["6: bababab"] --> 4["4: babab"]
3["3: baba"] --> 1["1: ba"]
5["5: bababa"] --> 3["3: baba"]
7["7: babababa"] --> 5["5: bababa"]
class 0,1 root
从图上读
这也正是 dp 方程的树形解释:
时 :继承父节点的答案——非根节点沿树向上冒泡,最终停在某个根上; 时自己是根, (自身长度 = 无 border)。
而
每个位置
代码
/**
* 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-08-05 12:00
* update_at: 2026-08-05 12:00
*/
/* P3435 [POI 2006] OKR-Periods of Words */
/* 核心观察:
* 1. 前缀的所有 border 按长度严格嵌套成一条链(沿 pi 链递减)。
* 2. 前缀 i 的最长真周期长度 = 长度 - 最短非空 border。
* 3. mini[i] 沿 pi 链跳到底即可:mini[i] = mini[pi[i]-1];无 border 时贡献 0。
* 下标约定与 rbook 文章《KMP 字符串匹配》一致:从 0 开始。 */
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000005;
int n;
char s[MAXN];
int pi[MAXN]; // pi[i]:s[0..i] 的最长相等真前后缀长度,pi[0] = 0
int mini[MAXN]; // mini[i]:s[0..i] 的最短非空 border 长度;无 border 时为 i+1
// 前缀函数模板(与 rbook 文章《KMP 字符串匹配》一致,0-indexed)
void build_prefix_function() {
for (int i = 1, j = 0; i < n; i++) {
while (j > 0 && s[i] != s[j]) {
j = pi[j - 1]; // 失配:回退到上一个可能成立的长度
}
if (s[i] == s[j]) j++;
pi[i] = j;
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
cin >> s;
build_prefix_function();
long long ans = 0;
for (int i = 0; i < n; i++) {
int len = i + 1;
if (pi[i] == 0) {
mini[i] = len; // 没有非空 border:不存在真周期
} else {
mini[i] = mini[pi[i] - 1]; // 沿 pi 链跳到底:最短非空 border
}
ans += len - mini[i]; // 最长真周期长度 = len - 最短 border
}
cout << ans << '\n';
return 0;
}复杂度
前缀函数与递推各
总结
本题的核心卡点不是 KMP 本身,而是把题面里的"周期"翻译成"border":
- 最长周期 → 最短非空 border;
- 最短 border → 在
链上递推, 每位置。
以后看到"周期 / 重复串",先想 border;看到"所有 border",先想
图示解析
以
border 链(沿 pi 反复跳):
pi[7] = 6 "bababa"
pi[5] = 4 "baba" ← pi[pi[7]-1] = pi[5]
pi[3] = 2 "ba" ← pi[pi[5]-1] = pi[3]
pi[1] = 0 链底
最短非空 border = 2("ba")→ 最长周期 = 8 - 2 = 6读图方法:border 链每步跳到更短的前缀(下标