求串的全部 border 长度:KMP 前缀函数只给最长 border,沿 pi 链反复回溯即可枚举全部,线性时间。
OJ: roj
题目 ID: 1458
难度:普及+/提高-
标签:KMP字符串前缀函数
创建: 2026-09-30 11:53
更新: 2026-10-07 11:01
形式化题目
给出若干字符串,对每个字符串
即所有既是
ababcababababcabab 的答案是 2 4 9 18,aaaaa 的答案是 1 2 3 4 5——后者说明输出量本身可以达到
正解
思路
一句话本质:题目要的就是全部 border,而 KMP 的前缀函数
朴素做法为什么不行?
最直接的想法是枚举每个长度
for k in 1..n:
if s[0:k] == s[n-k:n]:
记录 k它是对的,但每验证一个 aaaa...a 要能把整个前缀比完才确认匹配,总代价
在
把条件翻译成 border。
KMP 只给最长 border,不够用。
aaaa...a 就要输出
关键性质:所有 border 构成一条链。
设
换句话说,"
即从
还差一个平凡 border。
题目要求含
链上除
以样例 ababcababababcabab(
graph LR
A["18<br/>整串(平凡 border)"] --> B["9<br/>ababcabab"]
B --> C["4<br/>abab"]
C --> D["2<br/>ab"]
D --> E["0<br/>空串,链终止"]
每个节点是一个 border 长度,箭头是一次 j = pi[j] 跳转。长度严格递减,走到
链上的每一跳都能在
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | 1 | 2 | 0 | 1 | 2 | 3 | 4 | 3 | 4 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
对照表格读链:起点 2 4 9 18。注意
输出顺序不能忘。
回溯天然从大到小,而题目要递增,所以要翻转一次。另外从 aaaaa 可以看出,链长为
复杂度:求
代码
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 12:01
import sys
def prefix_function(pattern: bytes) -> list[int]:
"""pi[i] = pattern 的前 i 个字符里,最长的"既是真前缀又是真后缀"的长度。"""
n = len(pattern)
pi = [0] * (n + 1)
for i in range(1, n):
j = pi[i] # 先继承上一个位置的最长 border
while j and pattern[i] != pattern[j]: # 失配就沿 border 链回退
j = pi[j]
if pattern[i] == pattern[j]:
j += 1
pi[i + 1] = j
return pi
def border_chain(pi: list[int]) -> list[str]:
"""从链尾回溯得到全部 border 长度(含整串),升序返回。"""
lengths: list[str] = []
j = len(pi) - 1 # 整串本身一定是一个 border
while j: # 跳到 0 说明链走完,空串不算 border
lengths.append(str(j))
j = pi[j]
return lengths[::-1] # 回溯是降序的,翻转成题目要求的递增
def solve() -> None:
out = [" ".join(border_chain(prefix_function(word)))
for word in sys.stdin.buffer.read().split()]
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-05 02:31
* update_at: 2026-10-05 02:31
*/
#include <iostream>
#include <string>
using namespace std;
typedef long long ll;
const int MAXN = 400005; // 题目给定所有字符串总长不超过 4×10^5
char s[MAXN]; // 输入串,下标从 1 开始,和题面对应
int pref[MAXN]; // pref[i] = s[1..i] 的最长真 border 长度(真 = 不等于自身)
int chain[MAXN]; // chain[1..cnt] 存当前串的全部 border 长度(回溯得到的是降序)
// 求字符串 s[1..n] 的前缀函数
void build_prefix_function(ll n) {
pref[1] = 0;
for (ll i = 2; i <= n; i++) {
ll j = pref[i - 1];
// 失配就沿 border 链回退,直到能继续匹配或退到空串
while (j > 0 && s[i] != s[j + 1]) {
j = pref[j];
}
if (s[i] == s[j + 1]) {
j++;
}
pref[i] = j;
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
while (cin >> (s + 1)) {
ll n = 0;
while (s[n + 1] != '\0') {
n++;
}
build_prefix_function(n);
// 全部 border:整串 n 是平凡 border,再沿 pref 链反复回退
ll cnt = 0;
ll j = n;
while (j > 0) {
cnt++;
chain[cnt] = j;
j = pref[j];
}
// 回溯顺序是降序的,倒着输出即为题目要求的递增顺序
for (ll k = cnt; k >= 1; k--) {
if (k < cnt) {
cout << ' ';
}
cout << chain[k];
}
cout << '\n';
}
return 0;
}复杂度
- 时间:
求前缀函数( while回退摊还线性)+走 border 链, ,合计 ;多行输入为各串长度之和的线性函数。 - 空间:
,一个长度为 的 pi列表,另有同量级的输入/输出缓冲。
总结
本题是 KMP 前缀函数最直接的用途之一:
- "既是前缀又是后缀"就是 border 的定义,问题转化为求全部 border。
只给最长真 border,但所有 border 恰好是从它出发反复取 得到的一条严格递减链——这一步是整个解法的枢纽。 - 链的起点要手工设为
(补上平凡 border),链尾到 自然终止,回溯结果反转后即为递增答案。
识别出"全部 border =