使用分块有序表维护动态多重集合,支持插入、删除、排名与前趋后继查询。
OJ: roj
题目 ID: 1568
难度:提高
标签:数据结构平衡树分块
创建: 2026-09-30 18:48
更新: 2026-10-07 13:50
形式化题目
维护一个动态的多重整数集合
- 插入:向集合
中加入数 。 - 删除:从集合
中移除一个数 (若有多个相同数,仅删除一个)。 - 查询数
的排名:定义为集合中严格小于 的元素个数 。 - 查询排名为
的数:即查询集合从小到大排序后的第 个数( -indexed)。 - 查询数
的前趋:求集合中严格小于 且数值最大的数。 - 查询数
的后继:求集合中严格大于 且数值最小的数。
对于操作
正解
思路
本题是平衡树的经典模板题。在 Python 环境下,由于面向对象指针结构的递归和内存分配常数较大,手写二叉平衡树(如 Treap、Splay)极易遭遇递归爆栈或运行超时。
我们采用**分块有序表(Block List)**来实现:
-
结构组织: 维护一个由多个列表构成的容器
blocks,满足:- 每个子列表内部保持非降序;
- 块与块之间保持全局有序(即前一块的尾元素
后一块的头元素)。
-
核心操作:
- 插入:顺序检查各块的末尾元素。若目标值
块尾元素,使用 bisect.insort插入到该块内;若所有块尾都小于,则插入最后一个块。若单块长度膨胀超过阈值 ,触发全局展平重构。 - 删除:定位到包含
的块,二分定位并执行 pop,单次耗时。 - 排名查询:遍历各块,若全块元素均
,累加整块长度;否则在首个尾元素 的块内二分计算严格小于 的个数,总排名加 即得。 - 第
小查询:顺序扫描各块长度,用 依次扣减块长,直到落在某块内部,直接通过下标取出对应元素。 - 前趋与后继:
- 前趋:维护可能的最大前趋候选,一旦某个块的第一个元素
,后面所有块必然更大,即可提前剪枝。 - 后继:跳过末尾元素
的块,在第一个包含更大元素的块中使用 bisect_right取得答案。
- 前趋:维护可能的最大前趋候选,一旦某个块的第一个元素
- 插入:顺序检查各块的末尾元素。若目标值
-
分块平衡与重构: 设定块大小基准
。当单块长度超过 时,将所有块展平后按大小 重新划分为若干块,均摊复杂度仅为 。
graph LR
subgraph Blocks["分块有序表结构"]
B1["块 1: [2, 5, 8]"] --> B2["块 2: [12, 15, 19]"]
B2 --> B3["块 3: [24, 30, 35]"]
end
代码
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 18:48
# update_at: 2026-09-30 18:48
import bisect
import math
import sys
def add_val(blocks: list[list[int]], block_size: int, val: int) -> None:
"""在分块有序结构中插入元素 val。"""
for b in blocks:
if not b or val <= b[-1]:
bisect.insort(b, val)
if len(b) > 2 * block_size:
rebuild(blocks, block_size)
return
if not blocks:
blocks.append([val])
else:
bisect.insort(blocks[-1], val)
if len(blocks[-1]) > 2 * block_size:
rebuild(blocks, block_size)
def remove_val(blocks: list[list[int]], block_size: int, val: int) -> None:
"""在分块有序结构中删除一个 val。"""
for b in blocks:
if b and val <= b[-1]:
idx = bisect.bisect_left(b, val)
if idx < len(b) and b[idx] == val:
b.pop(idx)
return
def query_rank(blocks: list[list[int]], val: int) -> int:
"""查询 val 的排名(严格小于 val 的元素个数 + 1)。"""
smaller = 0
for b in blocks:
if not b:
continue
if b[-1] < val:
smaller += len(b)
else:
smaller += bisect.bisect_left(b, val)
break
return smaller + 1
def query_kth(blocks: list[list[int]], k: int) -> int:
"""查询排名为 k(即第 k 小,1-indexed)的数。"""
cur = k
for b in blocks:
if cur <= len(b):
return b[cur - 1]
cur -= len(b)
return -1
def query_prev(blocks: list[list[int]], val: int) -> int:
"""求小于 val 且最大的数(前趋)。"""
ans = -float('inf')
for b in blocks:
if not b or b[0] >= val:
break
if b[-1] < val:
ans = b[-1]
else:
idx = bisect.bisect_left(b, val)
if idx > 0:
ans = b[idx - 1]
break
return int(ans)
def query_next(blocks: list[list[int]], val: int) -> int:
"""求大于 val 且最小的数(后继)。"""
for b in blocks:
if not b or b[-1] <= val:
continue
idx = bisect.bisect_right(b, val)
if idx < len(b):
return b[idx]
return -1
def rebuild(blocks: list[list[int]], block_size: int) -> None:
"""重构分块以维持块大小均匀。"""
flattened: list[int] = [x for b in blocks for x in b]
blocks.clear()
for i in range(0, len(flattened), block_size):
blocks.append(flattened[i:i + block_size])
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
try:
n = next(data)
except StopIteration:
return
block_size = max(100, int(math.isqrt(n) * 2))
blocks: list[list[int]] = []
out: list[int] = []
for _ in range(n):
opt = next(data)
x = next(data)
if opt == 1:
add_val(blocks, block_size, x)
elif opt == 2:
remove_val(blocks, block_size, x)
elif opt == 3:
out.append(query_rank(blocks, x))
elif opt == 4:
out.append(query_kth(blocks, x))
elif opt == 5:
out.append(query_prev(blocks, x))
elif opt == 6:
out.append(query_next(blocks, x))
sys.stdout.write('\n'.join(map(str, out)) + '\n')
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 08:58
* update_at: 2026-10-05 08:58
*/
// main.cpp:普通平衡树,用分块有序表实现(对应题解的主算法)。
// 结构约定:blocks 中每个子列表内部非降序,块间整体有序,
// 即前一块的最大值 <= 后一块的最小值。
#include <cstdio>
#include <vector>
#include <algorithm>
#include <cmath>
#include <iostream>
using namespace std;
typedef long long ll;
vector<vector<ll> > blocks; // blocks[i] 为一个有序块,块内非降序
ll block_size; // 基准块大小 B,单块超过 2B 时整体重构
// 把所有块展平后按 block_size 重新均匀划分,用于插入膨胀后的平衡。
void rebuild_blocks() {
vector<ll> flat;
for (int i = 0; i < (int)blocks.size(); i++) {
for (int j = 0; j < (int)blocks[i].size(); j++) {
flat.push_back(blocks[i][j]);
}
}
blocks.clear();
for (int i = 0; i < (int)flat.size(); i += block_size) {
vector<ll> part;
for (int j = i; j < (int)flat.size() && j < i + block_size; j++) {
part.push_back(flat[j]);
}
blocks.push_back(part);
}
}
// 插入 val:找到第一个尾元素 >= val 的块插入,找不到就插入最后一块。
void insert_val(ll val) {
if (blocks.empty()) {
vector<ll> part;
part.push_back(val);
blocks.push_back(part);
return;
}
for (int i = 0; i < (int)blocks.size(); i++) {
vector<ll> &b = blocks[i];
if (b.empty()) {
continue;
}
if (val <= b[b.size() - 1]) {
vector<ll>::iterator pos = lower_bound(b.begin(), b.end(), val);
b.insert(pos, val);
if ((ll)b.size() > 2 * block_size) {
rebuild_blocks();
}
return;
}
}
// 所有块的尾元素都小于 val,直接放入最后一块
vector<ll> &last = blocks[blocks.size() - 1];
vector<ll>::iterator pos = lower_bound(last.begin(), last.end(), val);
last.insert(pos, val);
if ((ll)last.size() > 2 * block_size) {
rebuild_blocks();
}
}
// 删除一个 val:定位到可能含 val 的块,二分找到后删除一个。
void remove_val(ll val) {
for (int i = 0; i < (int)blocks.size(); i++) {
vector<ll> &b = blocks[i];
if (b.empty()) {
continue;
}
if (val <= b[b.size() - 1]) {
vector<ll>::iterator pos = lower_bound(b.begin(), b.end(), val);
if (pos != b.end() && *pos == val) {
b.erase(pos);
}
return;
}
}
}
// 查询 val 的排名:严格小于 val 的个数 + 1。
ll query_rank(ll val) {
ll smaller = 0;
for (int i = 0; i < (int)blocks.size(); i++) {
vector<ll> &b = blocks[i];
if (b.empty()) {
continue;
}
if (b[b.size() - 1] < val) {
smaller += (ll)b.size(); // 整块都比 val 小
} else {
smaller += lower_bound(b.begin(), b.end(), val) - b.begin();
break; // 后面的块只会更大,无需继续统计
}
}
return smaller + 1;
}
// 查询第 k 小(k 从 1 开始)的数。
ll query_kth(ll k) {
ll cur = k;
for (int i = 0; i < (int)blocks.size(); i++) {
vector<ll> &b = blocks[i];
if (cur <= (ll)b.size()) {
return b[cur - 1];
}
cur -= (ll)b.size();
}
return -1;
}
// 查询前趋:小于 val 且最大的数。
ll query_prev(ll val) {
int found = 0;
ll ans = 0;
for (int i = 0; i < (int)blocks.size(); i++) {
vector<ll> &b = blocks[i];
if (b.empty()) {
continue;
}
if (b[0] >= val) {
break; // 该块及之后所有元素都 >= val
}
if (b[b.size() - 1] < val) {
ans = b[b.size() - 1]; // 整块都是候选,取块内最大
found = 1;
} else {
vector<ll>::iterator pos = lower_bound(b.begin(), b.end(), val);
if (pos != b.begin()) {
ans = *(pos - 1);
found = 1;
}
break;
}
}
if (!found) {
return -1; // 题目保证前驱存在,这里只作兜底
}
return ans;
}
// 查询后继:大于 val 且最小的数。
ll query_next_val(ll val) {
for (int i = 0; i < (int)blocks.size(); i++) {
vector<ll> &b = blocks[i];
if (b.empty()) {
continue;
}
if (b[b.size() - 1] <= val) {
continue; // 整块都 <= val,不是后继
}
vector<ll>::iterator pos = upper_bound(b.begin(), b.end(), val);
if (pos != b.end()) {
return *pos;
}
}
return -1; // 题目保证后继存在,这里只作兜底
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
ll n;
if (!(cin >> n)) {
return 0;
}
block_size = 2 * (ll)sqrt((double)n) + 10; // 取 B ≈ 2√n,兼顾块内与块间代价
for (ll i = 0; i < n; i++) {
ll opt;
ll x;
cin >> opt >> x;
if (opt == 1) {
insert_val(x);
} else if (opt == 2) {
remove_val(x);
} else if (opt == 3) {
cout << query_rank(x) << '\n';
} else if (opt == 4) {
cout << query_kth(x) << '\n';
} else if (opt == 5) {
cout << query_prev(x) << '\n';
} else if (opt == 6) {
cout << query_next_val(x) << '\n';
}
}
return 0;
}复杂度
- 时间复杂度:令块大小
,总块数为 。单次插入、删除、查询排名、前趋与后继均由 次块间定位及 的块内二分与位移构成;平摊重构复杂度为 。单次操作平摊复杂度为 ,总时间复杂度为 。在 Python 中得益于内置 C 底层操作,性能极优。 - 空间复杂度:存储集合中所有的元素,总空间复杂度为
。
总结
在处理值域较大或动态有序集合问题时,分块有序表兼顾了简单性与高效性,在高级语言中配合标准库二分能发挥出极强的常数优势。