形式化题目

给出若干字符串,对每个字符串 ss(∣s∣=n|s| = n),求集合

B(s)={ k∣1⩽k⩽n, s[1..k]=s[n−k+1..n] } B(s) = \{\, k \mid 1 \leqslant k \leqslant n,\ s[1..k] = s[n-k+1..n] \,\}

即所有既是 ss 的前缀、又是 ss 的后缀的子串长度,按递增顺序全部输出。

k=nk = n 恒成立(整串与自身相等),所以答案非空;判定方式是对应位置逐字符等值。样例 ababcababababcabab 的答案是 2 4 9 18,aaaaa 的答案是 1 2 3 4 5——后者说明输出量本身可以达到 O(n)O(n)。

正解

思路

一句话本质:题目要的就是全部 border,而 KMP 的前缀函数 pipi 把所有 border 组织成一条"从最长 border 出发、反复取 pipi"的链,沿链走一遍就能把它们全部取出来。

朴素做法为什么不行?

最直接的想法是枚举每个长度 kk,把前 kk 位和后 kk 位逐位比较:

text
for k in 1..n:
    if s[0:k] == s[n-k:n]:
        记录 k

它是对的,但每验证一个 kk 都要付 O(k)O(k) 的代价——例如 aaaa...a 要能把整个前缀比完才确认匹配,总代价

∑k=1nO(k)=O(n2) \sum_{k=1}^{n} O(k) = O(n^2)

在 n=4×105n = 4 \times 10^5 时约 1.6×10111.6 \times 10^{11} 次比较,无法通过。浪费在哪? 每个 kk 被当成独立问题,可 s[1..k]s[1..k] 本身就是 ss 的前缀,这些前缀的 border 之间有着强包含关系,却被完全忽略了。

把条件翻译成 border。

s[1..k]=s[n−k+1..n]s[1..k] = s[n-k+1..n] 说的正是"长度为 kk 的前缀等于长度为 kk 的后缀",也就是 kk 是 ss 的 border。于是问题变成:求一个串的全部 border 长度。

KMP 只给最长 border,不够用。

pi[i]pi[i] 的定义是 s[1..i]s[1..i] 的最长真 border(真 = 不等于自身),递推求它的代价是 O(n)O(n)。但它一次只给一个长度,而答案可能有 nn 个(aaaa...a 就要输出 nn 个长度),必须想办法把"最长"扩成"全部"。

关键性质:所有 border 构成一条链。

设 bb 是 s[1..i]s[1..i] 的真 border。因为 pi[i]pi[i] 最长,所以 b⩽pi[i]b \leqslant pi[i];又 s[1..b]s[1..b] 是 s[1..pi[i]]s[1..pi[i]] 的前缀(长度不超过它),同时 s[1..b]s[1..b] 也是 s[1..i]s[1..i] 的后缀、而 s[1..pi[i]]s[1..pi[i]] 是 s[1..i]s[1..i] 的后缀且更长,所以 s[1..b]s[1..b] 还是 s[1..pi[i]]s[1..pi[i]] 的后缀。两者合起来说明 bb 一定是 s[1..pi[i]]s[1..pi[i]] 的 border。

换句话说,"s[1..i]s[1..i] 的次长 border"恰好是 pi[pi[i]]pi[pi[i]],再下一项是 pi[pi[pi[i]]]pi[pi[pi[i]]],以此类推:

{ s[1..i] 的全部真 border }={ pi[i], pi[pi[i]], pi[pi[pi[i]]], … }∖{0} \{\, s[1..i] \text{ 的全部真 border} \,\} = \{\, pi[i],\ pi[pi[i]],\ pi[pi[pi[i]]],\ \ldots \,\} \setminus \{0\}

即从 pi[i]pi[i] 出发反复取 pipi,就枚举了全部真 border,每一步长度严格减小,链长不超过 nn。

还差一个平凡 border。

题目要求含 nn 本身,而 pi[n]pi[n] 只是真 border(pi[n]<npi[n] < n)。所以链的起点不设为 pi[n]pi[n],而是直接设为 nn:

n  ⟶  pi[n]  ⟶  pi[pi[n]]  ⟶  ⋯  ⟶  0 n \;\longrightarrow\; pi[n] \;\longrightarrow\; pi\bigl[pi[n]\bigr] \;\longrightarrow\; \cdots \;\longrightarrow\; 0

链上除 00 之外的每个值都是答案。

以样例 ababcababababcabab(n=18n = 18)为例,它的 border 链如下。

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] 跳转。长度严格递减,走到 00 说明已经没有更短的 border;链上非零节点的个数就是答案的个数。

链上的每一跳都能在 pipi 数组里查到:

ii 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
pi[i]pi[i] 0 0 1 2 0 1 2 3 4 3 4 3 4 5 6 7 8 9

对照表格读链:起点 1818 不在表内(它是平凡 border,手工补上),然后 pi[18]=9pi[18] = 9、pi[9]=4pi[9] = 4、pi[4]=2pi[4] = 2、pi[2]=0pi[2] = 0 终止。收集到的 18,9,4,218, 9, 4, 2 是降序的,反转后正是题目要求的 2 4 9 18。注意 pi[18]=9pi[18] = 9 而 pi[9]=4pi[9] = 4,链的每一跳都来自表格中"当前长度那一列",这是手工模拟时最容易看错的地方。

输出顺序不能忘。

回溯天然从大到小,而题目要递增,所以要翻转一次。另外从 aaaaa 可以看出,链长为 nn 时输出 nn 个数,输出量本身是 O(n)O(n),实现上必须一次性拼好再写出,不能每个数字一次 I/O。

复杂度:求 pipi 是 O(n)O(n)(回退指针摊还 O(1)O(1)),走链是 O(∣B∣)⊆O(n)O(|B|) \subseteq O(n),总计 O(n)O(n);空间 O(n)O(n) 存 pipi。

代码

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-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++ 版(同一算法):

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: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;
}

复杂度

  • 时间:O(n)O(n) 求前缀函数(while 回退摊还线性)+ O(∣B∣)O(|B|) 走 border 链,∣B∣⩽n|B| \leqslant n,合计 O(n)O(n);多行输入为各串长度之和的线性函数。
  • 空间:O(n)O(n),一个长度为 n+1n+1 的 pi 列表,另有同量级的输入/输出缓冲。

总结

本题是 KMP 前缀函数最直接的用途之一:

  1. "既是前缀又是后缀"就是 border 的定义,问题转化为求全部 border。
  2. pi[i]pi[i] 只给最长真 border,但所有 border 恰好是从它出发反复取 pipi 得到的一条严格递减链——这一步是整个解法的枢纽。
  3. 链的起点要手工设为 nn(补上平凡 border),链尾到 00 自然终止,回溯结果反转后即为递增答案。

识别出"全部 border = pipi 链"这个结构之后,O(n2)O(n^2) 的逐位比较就变成了 O(n)O(n) 的一次预处理加一次链上遍历。同一条链也是更难的 border 类题目的共同起点,例如沿链求每个前缀的最短 border、把链关系建成 border 树做统计等。