用正反双向滚动哈希把每个块及其反转编码成同一个 O(1) 整数指纹,逐 k 去重计数取最优。
OJ: roj
题目 ID: 1461
难度:提高+/省选-
标签:
创建: 2026-09-30 12:05
更新: 2026-10-07 11:01
形式化题目
给定长度为
末尾不足
记
并输出
正解
思路
朴素做法的瓶颈在哪里
对每个
- 若逐字符与其他块比较,单块最坏
,整体退化到 ; - 若把块存成
tuple并用min(t, t[::-1])去重,t[::-1]要复制个元素, 实测 时约 166 秒。
所以要做的是:给每个块一个定长的整数指纹,让单块编码降到
滚动哈希: 取块的指纹
先做三项
- 幂表
; - 正向前缀哈希
; - 反向前缀哈希
。
于是位置
都可以在
关键观察: 就是反转块的正读哈希
正好是把块
设块为
两个指纹在反转下互换。因此定义
就有 len(set(...)) 就是这个
为什么用 而不是大素数
素数取模需要真正的取余运算,CPython 对 60 位以上整数的 % 开销明显。
改用 & ((1 << 64) - 1),实测把最坏用例从 1.05 s 压到 0.74 s,
这正是本解能压进 1000 ms 的关键。可行性依赖两点:
- 底数
取奇数( ),与 互素,于是 在环 中可逆, 区间还原公式仍然成立; - 减法可能得到负数,
& MASK等价于"加再取模",结果正确。
下面的表格是样例在 code,因此只算一个不同块。
| 起点 |
块 | |||
|---|---|---|---|---|
| 0 | (1,1) |
1000004 | 1000004 | 1000004 |
| 2 | (1,2) |
1000005 | 2000007 | 1000005 |
| 4 | (2,2) |
2000008 | 2000008 | 2000008 |
| 6 | (3,3) |
3000012 | 3000012 | 3000012 |
| 8 | (3,1) |
3000010 | 1000006 | 1000006 |
| 10 | (2,3) |
2000009 | 3000011 | 2000009 |
| 12 | (3,1) |
3000010 | 1000006 | 1000006 |
| 14 | (2,2) |
2000008 | 2000008 | 2000008 |
| 16 | (1,3) |
1000006 | 3000010 | 1000006 |
| 18 | (3,2) |
3000011 | 2000009 | 2000009 |
表中 10 个块只产生 6 种不同的 code,与样例所说的 (3,1)、(1,3) 共享 1000006,(2,3)、(3,2) 共享 2000009。
算法主干
graph TD
A["读入 n 与序列 a"] --> B["预处理 pw / pre / suf,均为 O(n)"]
B --> C["k 从 1 枚举到 n"]
C --> D["对每个完整块起点 p = 0, k, 2k, ...<br/>O(1) 算出 code = min(fwd, bwd)"]
D --> E["len(set(codes)) 即为 cnt(k)"]
E --> F{"cnt(k) 与 best 比较"}
F -->|更大| G["best = cnt(k),ks 重置为 [k]"]
F -->|相等| H["ks 追加 k"]
F -->|更小| I["跳过"]
G --> J{"k 枚举完?"}
H --> J
I --> J
J -->|否| C
J -->|是| K["输出 best、len(ks) 与 ks 中所有 k"]
每轮只做
代码
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 12:10
# update_at: 2026-09-30 12:10
import sys
MASK = (1 << 64) - 1 # 取模用位与代替 %:底数为奇数时 2^64 取模仍是合法滚动哈希
BASE = 1_000_003 # 奇数底数,保证块哈希能用前缀哈希 O(1) 还原
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n = next(data)
a = [next(data) for _ in range(n)]
# 幂表、正向前缀哈希、反向(从右往左)前缀哈希,三张表都是 O(n) 预处理
pw = [1] * (n + 1)
pre = [0] * (n + 1) # pre[i] = a[0..i-1] 的哈希
suf = [0] * (n + 1) # suf[i] = a[i..n-1] 的哈希(越靠左权重越高)
for i in range(1, n + 1):
pw[i] = (pw[i - 1] * BASE) & MASK
for i, x in enumerate(a):
pre[i + 1] = (pre[i] * BASE + x) & MASK
for i in range(n - 1, -1, -1):
suf[i] = (suf[i + 1] * BASE + a[i]) & MASK
best, ks = 0, []
for k in range(1, n + 1):
wk = pw[k]
# 每 k 个珠子的块取一次(末尾不足 k 的丢弃);正读与倒读哈希取小者,
# 于是 (1,2,3) 与 (3,2,1) 落到同一个编码,去重后即为不同子串数
cnt = len({
min((pre[p + k] - pre[p] * wk) & MASK, (suf[p] - suf[p + k] * wk) & MASK)
for p in range(0, n - k + 1, k)
})
if cnt > best:
best, ks = cnt, [k]
elif cnt == best:
ks.append(k)
print(best, len(ks))
print(' '.join(map(str, ks)))
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:40
* update_at: 2026-10-05 02:40
*/
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
typedef long long ll;
const int MAXN = 200005;
// 正反两向滚动哈希,2^64 自然溢出等价于模 2^64,底数取奇数保证可逆
typedef unsigned long long ull;
const ull BASE = 1000003ULL; // 奇数底数,B^k 在模 2^64 下可逆
ll n; // 珠子个数
ll a[MAXN]; // a[i] 为第 i 个珠子的颜色(下标从 1 开始)
ull pw[MAXN]; // pw[i] = BASE^i
ull pre[MAXN]; // pre[i] = a[1..i] 的正向哈希
ull suf[MAXN]; // suf[i] = a[i..n] 的反向哈希(越靠左权重越高)
ull block_code[MAXN]; // 当前 k 下每个完整块的编码(正读、倒读哈希取小)
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (ll i = 1; i <= n; i++) {
cin >> a[i];
}
// 三张预处理表都是 O(n)
pw[0] = 1ULL;
for (ll i = 1; i <= n; i++) {
pw[i] = pw[i - 1] * BASE;
}
pre[0] = 0ULL;
for (ll i = 1; i <= n; i++) {
pre[i] = pre[i - 1] * BASE + a[i];
}
suf[n + 1] = 0ULL;
for (ll i = n; i >= 1; i--) {
suf[i] = suf[i + 1] * BASE + a[i];
}
ll best = -1; // 目前找到的最大不同块数
vector<ll> ks; // 取得最大值的所有 k(升序)
for (ll k = 1; k <= n; k++) {
ll m = n / k; // 完整块个数,末尾不足 k 的珠子丢弃
// 每个块用正读哈希 fwd 与倒读哈希 bwd 的较小者作为编码;
// 块与其反转的编码相同,于是同一个编码只算一个不同块
for (ll idx = 0; idx < m; idx++) {
ll p = idx * k + 1; // 块起点(1-based)
ull fwd = pre[p + k - 1] - pre[p - 1] * pw[k];
ull bwd = suf[p] - suf[p + k] * pw[k];
block_code[idx] = fwd < bwd ? fwd : bwd;
}
// 排序后统计不同编码个数,即为 cnt(k)
sort(block_code, block_code + m);
ll cnt = 0;
for (ll idx = 0; idx < m; idx++) {
if (idx == 0 || block_code[idx] != block_code[idx - 1]) {
cnt++;
}
}
if (cnt > best) {
best = cnt;
ks.clear();
ks.push_back(k);
} else if (cnt == best) {
ks.push_back(k);
}
}
cout << best << ' ' << ks.size() << '\n';
for (size_t i = 0; i < ks.size(); i++) {
if (i > 0) {
cout << ' ';
}
cout << ks[i];
}
cout << '\n';
return 0;
}复杂度
- 时间:预处理
,枚举 的总编码次数 , 故 。 - 空间:三张长度
的表,加上单个 的哈希集合, 。
与数据仓里 OI 官方的替代解 kor2.cpp(正反双向哈希 + 排序去重)同阶,
优于 kor.cpp 的后缀数组解法
总结
- 本题的规模瓶颈是"块的总数
还能接受,但单块处理必须 "。 - 正反两向前缀哈希让"块"和"块的反转"都能在
内取到指纹; 再对两个指纹取 ,就把"反转视为相同"编码进了同一个整数。 - Python 侧用
取模配合位与 & MASK(底数取奇数以保证可逆)压掉取模开销, 最坏用例从 1.05 s 降到 0.74 s,实测 32 组官方数据在 1000 ms / 128 MB 内全部通过。