求字符串的最小周期长度。用整个字符串的最长 border 求能够生成接收片段的最短信号周期。
OJ: luogu
题目 ID: P4391
难度:普及/提高-
标签:KMP周期border哈希字符串pythoncpp
日期: 2026-07-16 19:57
题意
给出一段可能从周期信号中截取的字符串,求原信号最短可能长度。
思路
周期与 border 是同一枚硬币的两面:
KMP 法
求前缀函数
哈希法
用滚动哈希直接枚举长度验证。
周期与 border 的数学证明
定义:
定理:
证明(
(
图解:样例 (n=8)周期
text
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ ← 位置
├───┼───┼───┼───┼───┼───┼───┼───┤
│ c │ a │ b │ c │ a │ b │ c │ a │ ← 字符
└───┴───┴───┴───┴───┴───┴───┴───┘
├───── s[1..5] ─────┤
├───── s[4..8] ─────┤
↑ 错位 len=3
c a b c a
c a b c a ← 5 个字符逐位相等逐位验证周期定义
text
i=1: s[1]=c, s[4]=c ✓
i=2: s[2]=a, s[5]=a ✓
i=3: s[3]=b, s[6]=b ✓
i=4: s[4]=c, s[7]=c ✓
i=5: s[5]=a, s[8]=a ✓答案
哈希原理
滚动哈希把前缀视为
取子串
以
| 表达式 | 值 |
|---|---|
减去
哈希法枚举周期
定理给出周期的充要条件:
代码
KMP 法:
python
import sys
def build_prefix(s):
pi = [0] * len(s)
j = 0
for i in range(1, len(s)):
while j and s[i] != s[j]:
j = pi[j - 1]
if s[i] == s[j]:
j += 1
pi[i] = j
return pi
n, word = [x.decode() for x in sys.stdin.buffer.read().split()]
pi = build_prefix(word)
print(int(n) - pi[-1])cpp
/**
* Radio Transmission - KMP 法
*
* 答案 = n - 字符串最长 border
* 只需求模式串自身的 prefix 函数,再取 pref[n-1]
*/
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
string s;
cin >> n >> s;
vector<int> pref(n);
int j = 0;
for (int i = 1; i < n; ++i) {
while (j && s[i] != s[j])
j = pref[j - 1];
if (s[i] == s[j])
++j;
pref[i] = j;
}
cout << n - pref[n - 1] << '\n';
return 0;
}哈希实现:
cpp
#include <iostream>
#include <algorithm>
using namespace std;
typedef unsigned long long ull;
const int MAXN = 1000005;
const int P = 131;
ull h[MAXN], p[MAXN];
char s[MAXN];
int n;
void init_hash() {
p[0] = 1;
for (int i = 1; i <= n; ++i) {
h[i] = h[i - 1] * P + s[i];
p[i] = p[i - 1] * P;
}
}
ull get_hash(int l, int r) {
return h[r] - h[l - 1] * p[r - l + 1];
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> (s + 1);
init_hash();
// 暴力枚举循环节长度 len
for (int len = 1; len <= n; ++len) {
bool ok = true;
int _len = n-len;
ull pre = get_hash(1, _len);
ull suf = get_hash(n-_len+1 ,n);
if( pre == suf) {
cout << len << endl;
break;
}
}
return 0;
}
复杂度
时间和空间均为
总结
周期与 border 是同一件事的两种描述:周期长度 = 前缀长度 - border 长度。