分块预处理每个完整块对中正偶数种类数,询问时对两侧散段的互异值用「(值,位置) 排序键二分」算出总次数做二次补偿,强制在线下 O((n+m)√n)。
OJ: roj
题目 ID: 3126
难度:提高+/省选-
标签:分块python
创建: 2026-10-01 18:28
更新: 2026-10-10 11:30
形式化题目
给定长度为
询问强制在线:设上一个答案为
若
以样例
正解
思路
朴素做法的瓶颈。 最直接的做法是每个询问开计数器扫一遍区间,单次
分块拆解。 取块长
[ l .. 块bl的末尾 | 完整块 bl+1 .. br-1 | 块br的开头 .. r ]
左散段(<T 个) 中间完整块 右散段(<T 个)完整块部分的答案可以用一张预处理表
预处理
的新旧差
查询:散段二次补偿。 设中间完整块为
: 在中间完整块 中的次数(块计数的前缀和 查); : 在整个区间 的次数 左散段次数 右散段次数。
则
为什么恰好补对:完整块独有的值不在
Python 落地:把"逐值二分"变成"整组二分"。 上面每个散段值都要在序列里数出现次数,纯 Python 逐值二分太慢。把每个位置编码成一个键
整组值一次性向量化算完,单次询问只剩
两个容易踩的坑(都曾在小样例上被掩盖):
- 解密公式是
,转成 0 下标后等价于 ;写成 再减一,样例都过不了。 - 中间完整块是
,块 本身是散块。 要用 , 取 ;如果把 也当完整块,凡是 不在块末尾就会多算。
代码
/**
* 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-10 11:30
* update_at: 2026-10-10 11:30
*/
// 作诗:分块预处理「整块区间内出现正偶数次的数值个数」,询问补两端零散位置。
#include <cstdio>
#include <vector>
#include <algorithm>
#include <cstring>
typedef long long ll;
const ll T = 1000; // 分块大小
struct BlockVal {
int v; // 值
int c; // 该值在本块内的出现次数
};
int main() {
ll n, c, m;
if (scanf("%lld %lld %lld", &n, &c, &m) != 3) return 0; // 空输入安全返回
std::vector<ll> a(n);
for (ll i = 0; i < n; i++) {
scanf("%lld", &a[i]);
}
ll blocks = (n + T - 1) / T;
ll W = c + 1;
// pre[i][v] = 前 i 个块中值 v 的出现次数
std::vector<int> pre((blocks + 1) * W, 0);
std::vector<std::vector<BlockVal> > nz(blocks); // 每块里出现过的值与块内次数
std::vector<int> cntB(c + 1, 0);
std::vector<int> touched;
for (ll i = 0; i < blocks; i++) {
ll lo = i * T, hi = (i + 1) * T;
if (hi > n) hi = n;
touched.clear();
for (ll t = lo; t < hi; t++) {
int v = (int)a[t];
if (cntB[v] == 0) touched.push_back(v);
cntB[v]++;
}
memcpy(&pre[(i + 1) * W], &pre[i * W], W * sizeof(int)); // 先继承前缀
for (size_t t = 0; t < touched.size(); t++) {
int v = touched[t];
pre[(i + 1) * W + v] += cntB[v];
BlockVal bv;
bv.v = v;
bv.c = cntB[v]; // 块内计数不能只用「出现与否」
nz[i].push_back(bv);
cntB[v] = 0;
}
}
// g[i][j] = 完整块区间 [i, j] 内出现正偶数次的数值个数
std::vector<std::vector<int> > g(blocks, std::vector<int>(blocks, 0));
std::vector<int> cnt(c + 1, 0);
for (ll i = 0; i < blocks; i++) {
touched.clear();
ll cur = 0;
for (ll j = i; j < blocks; j++) {
for (size_t t = 0; t < nz[j].size(); t++) {
int v = nz[j][t].v;
ll before = cnt[v];
ll after = before + nz[j][t].c;
cnt[v] = (int)after;
bool db = (before % 2 == 0 && before >= 2); // 状态函数 f(x) = (x 为正偶数)
bool da = (after % 2 == 0 && after >= 2);
if (da && !db) cur++;
else if (!da && db) cur--;
if (before == 0) touched.push_back(v);
}
g[i][j] = (int)cur;
}
for (size_t t = 0; t < touched.size(); t++) cnt[touched[t]] = 0;
}
std::vector<int> ccnt(c + 1, 0);
std::vector<int> touch;
ll ans = 0; // 上一个询问的答案,强制在线解密用
for (ll q = 0; q < m; q++) {
ll ql, qr;
scanf("%lld %lld", &ql, &qr);
ll l = (ql + ans) % n;
ll r = (qr + ans) % n;
if (l > r) {
ll tmp = l; l = r; r = tmp;
}
ll bl = l / T, br = r / T;
touch.clear();
if (br - bl < 2) {
// 中间没有完整块:整段当作散段直接统计
for (ll i = l; i <= r; i++) {
int v = (int)a[i];
if (ccnt[v] == 0) touch.push_back(v);
ccnt[v]++;
}
ll res = 0;
for (size_t t = 0; t < touch.size(); t++) {
ll cntv = ccnt[touch[t]];
if (cntv >= 2 && cntv % 2 == 0) res++;
}
ans = res;
} else {
// 两侧散段的互异值:先在完整块答案上做补偿
ll lhi = (bl + 1) * T;
for (ll i = l; i < lhi; i++) {
int v = (int)a[i];
if (ccnt[v] == 0) touch.push_back(v);
ccnt[v]++;
}
for (ll i = br * T; i <= r; i++) {
int v = (int)a[i];
if (ccnt[v] == 0) touch.push_back(v);
ccnt[v]++;
}
ll res = g[bl + 1][br - 1];
for (size_t t = 0; t < touch.size(); t++) {
int v = touch[t];
ll tin = pre[br * W + v] - pre[(bl + 1) * W + v]; // 该值在中间完整块的次数
ll total = tin + ccnt[v];
if (total >= 2 && total % 2 == 0) res++; // 总次数为正偶数
if (tin >= 2 && tin % 2 == 0) res--; // 只在中间块里算成过正偶数
}
ans = res;
}
printf("%lld\n", ans);
for (size_t t = 0; t < touch.size(); t++) ccnt[touch[t]] = 0;
}
return 0;
}#!/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-10-01 18:28
# update_at: 2026-10-01 18:28
import numpy as np
import sys
T = 1000 # 分块大小:位置按每 T 个分成一块,块内散元素 ≤ 2T 个逐个补偿
def block_pair_table(bc: np.ndarray, blocks: int) -> list[list[int]]:
"""g[i][j] = 完整块区间 [i, j] 内出现正偶数次的数值个数。
逐行递增右端块:往 [i, j-1] 的计数里并入第 j 块,
只有第 j 块里出现过的值状态可能变化,增量只在这些值上算。
"""
g = [[0] * blocks for _ in range(blocks)]
cnt = np.zeros(bc.shape[1], np.int32) # 值 → 在块 [i, j] 中的出现次数
for i in range(blocks):
cnt[:] = 0
cur = 0
row = g[i]
for j in range(i, blocks):
nz = np.nonzero(bc[j])[0] # 第 j 块里出现过的值
before = cnt[nz]
after = before + bc[j][nz]
cnt[nz] = after
# 状态函数 f(x) = (x 为正偶数):新状态转 1、旧状态转 1,差值即增量
cur += int((((after & 1) == 0) & (after >= 2)).sum()
- (((before & 1) == 0) & (before >= 2)).sum())
row[j] = cur
return g
def solve() -> None:
data = sys.stdin.buffer.read().split()
nums = list(map(int, data))
n, c, m = nums[0], nums[1], nums[2]
a = np.array(nums[3:3 + n], dtype=np.int32)
qs = nums[3 + n:]
blocks = (n + T - 1) // T
bc = np.zeros((blocks, c + 1), np.int32) # 每块内各值的出现次数
np.add.at(bc, (np.arange(n) // T, a), 1)
pre = np.zeros((blocks + 1, c + 1), np.int32) # pre[i][v] = 前 i 块中 v 的次数
np.cumsum(bc, axis=0, out=pre[1:])
# 把 (值, 位置) 编码成一个 int64 键后全局排序:值 v 在位置区间 [lo, hi]
# 的出现次数 = 二分键 v*(n+1)+hi+1 与 v*(n+1)+lo 的差,可对整组值向量化
keys = np.sort(a.astype(np.int64) * (n + 1) + np.arange(n, dtype=np.int64))
g = block_pair_table(bc, blocks)
out: list[str] = []
ans = 0 # 上一个询问的答案,强制在线解密用
n1 = n + 1
for q in range(m):
l = (qs[2 * q] + ans) % n # 题面 L=(l+ans) mod n+1,转 0 下标后即 mod n
r = (qs[2 * q + 1] + ans) % n
if l > r:
l, r = r, l
bl, br = l // T, r // T
if br - bl < 2: # 中间没有完整块:整段当作散段直接统计
vu = np.unique(a[l:r + 1])
key = vu.astype(np.int64) * n1
t = np.searchsorted(keys, key + r + 1) - np.searchsorted(keys, key + l)
ans = int((((t & 1) == 0) & (t >= 2)).sum())
else:
# 两侧散段的互异值:先在完整块答案上做补偿
idx = np.concatenate((np.arange(l, (bl + 1) * T), np.arange(br * T, r + 1)))
vu = np.unique(a[idx])
key = vu.astype(np.int64) * n1
tin = pre[br][vu] - pre[bl + 1][vu] # 该值在中间完整块 bl+1..br-1 的次数
cl = (np.searchsorted(keys, key + (bl + 1) * T)
- np.searchsorted(keys, key + l)) # 左散段次数
cr = (np.searchsorted(keys, key + r + 1)
- np.searchsorted(keys, key + br * T)) # 右散段次数
t = tin + cl + cr
# 总次数为正偶数 → 加 1;只在中间块里算成过正偶数 → 撤回 1
ans = (g[bl + 1][br - 1]
+ int((((t & 1) == 0) & (t >= 2)).sum()
- (((tin & 1) == 0) & (tin >= 2)).sum()))
out.append(str(ans))
sys.stdout.write('\n'.join(out) + '\n')
if __name__ == "__main__":
solve()复杂度
设块数
- 预处理
: 次值级操作(numpy 向量化),空间 的前缀和表 + 的 表; - 单次询问:散段互异值
,去重与成对二分共 ,总计 级。
整体与 C++ 正解同阶(
总结
- 强制在线 +
封死离线做法,指向"预处理全局信息 + 询问少量补偿"的分块结构。 - 区间拆成 中间完整块 + 两侧散段:完整块答案查表
( ),散段值( 个互异值)逐值二次补偿。 - 状态函数
只看奇偶与是否非零,预处理扫描线和补偿公式因此都能增量维护。 - Python 实现要点:把 (值, 位置) 编成排序键,任意值集合在任意位置区间的出现次数用一次成对
searchsorted整组求出,避免逐值二分。 - 调试经验:解密公式的取模基数、"完整块右端是
不是 "这两处错误在极小的样例上都不会暴露,必须用大数据验证。
图示解析
样例的分块拆解与补偿计算
以样例
| 块 | |||
|---|---|---|---|
| 位置(1 下标) | 1, 2 | 3, 4 | 5 |
| 值 | 1, 2 | 2, 3 | 1 |
取询问
位置: 1 2 | 3 4 | 5
值: 1 2 | 2 3 | 1
~~ ----- ~~
[2,5] = 左散段{位置2: 值2} + 完整块 b2{值2, 值3} + 右散段{位置5: 值1}拆解的观察重点:完整块是
第一步先查表:
第二步对两侧散段的互异值并集
| 值 |
散段次数 | 总次数 |
修正量 |
|||
|---|---|---|---|---|---|---|
| 2 | 1(位置 3) | 1(位置 2) | 2 | 1 | 0 | |
| 1 | 0 | 1(位置 5) | 1 | 0 | 0 |
答案
这张表恰好演示了补偿公式的三种典型情形,值得逐行对照:
- 值 2 是"修正量非零"的情形:它在完整块内只出现 1 次(
, 没算它),加上散段后总次数变成 2,补偿 把它捡回来; - 值 1 是"散段独有"的情形:
, ,补偿只按全区间次数 计,不产生贡献; - 若某值在完整块内出现 2 次、加散段后变 3 次(比如把询问改成
时值 2 不受影响,但可以想象 内有两份值 2 的情形),则 、 ,修正量为 ——补偿会撤掉 中那份贡献。这正是"补偿是替换不是叠加":每个散段互异值的贡献都被从状态 替换成真实状态 。
对照 main.py 的查询分支:tin = pre[br][vu] - pre[bl+1][vu](前缀和差,右端取 cl、cr 用 (值, 位置) 排序键成对二分算两侧散段次数,最后 g[bl+1][br-1] + Σf(t) − Σf(tin) 与本表逐项对应。