把区间 [l,r] 的第 k 小看成两棵前缀权值线段树的计数相减,用可持久化线段树(主席树)在 O((n+m)log n) 内回答每个询问。
OJ: roj
题目 ID: 3121
难度:提高+/省选-
标签:主席树可持久化线段树离散化数据结构
创建: 2026-10-01 18:17
更新: 2026-10-10 11:30
形式化题目
给定长度为
正解
思路
朴素做法:对每个询问,把
第一个观察:把"区间"变成"前缀之差"。 数组是静态的,
第二个观察:相邻前缀只差一个数——可持久化。 前缀
查询:两棵树同步下降。 设
用样例 1 5 2 6 3 7 4 看查询 [2,5](即 5 2 6 3,求第 3 小):排序后为 2 3 5 6,第 3 小是 5。查询时先比较"左半区(较小的那些值)里有几个数",再决定向左还是向右,每层只看一次计数差:
值域(离散化后 7 个值: 1 2 3 4 5 6 7)
层级1 [1..7) 左半 {1,2,3} 有 2 个 (2,3) → k=3 > 2,去右半,k←3-2=1
层级2 [4..7) 左半 {4,5} 有 1 个 (5) → k=1 ≤ 1,去左半
层级3 [4..6) 左半 {4} 有 0 个 → 去右半,k 不变
层级4 [5..6) 只剩值 5 → 答案 5代码
/**
* 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
*/
// 区间第 k 小:可持久化权值线段树,版本区间相减。
#include <cstdio>
#include <vector>
#include <algorithm>
typedef long long ll;
const ll MAXNODE = 3000000;
struct Node {
int lc; // 左儿子编号
int rc; // 右儿子编号
int cnt; // 子树内数的个数
};
Node node[MAXNODE];
ll tot = 0;
std::vector<ll> vals;
std::vector<int> roots; // roots[i] = 插入前 i 个数后的树根
// 在 prev 版本上把离散化下标 pos 的计数 +1,返回新版本节点编号
int insert(int prev, ll lo, ll hi, ll pos) {
tot++;
int root = (int)tot;
node[root] = node[prev];
node[root].cnt++;
int cur = root, old = prev;
while (hi - lo > 1) {
ll mid = (lo + hi) / 2;
if (pos < mid) {
tot++;
int nd = (int)tot;
node[nd] = node[node[old].lc];
node[nd].cnt++;
node[cur].lc = nd;
cur = nd;
old = node[old].lc;
hi = mid;
} else {
tot++;
int nd = (int)tot;
node[nd] = node[node[old].rc];
node[nd].cnt++;
node[cur].rc = nd;
cur = nd;
old = node[old].rc;
lo = mid;
}
}
return root;
}
// 在版本区间 (u, v] 上求第 k 小的离散化下标
ll kth(int u, int v, ll k, ll lo, ll hi) {
while (hi - lo > 1) {
ll mid = (lo + hi) / 2;
ll left_cnt = node[node[v].lc].cnt - node[node[u].lc].cnt; // 值落在左半区的个数
if (k <= left_cnt) {
u = node[u].lc;
v = node[v].lc;
hi = mid;
} else {
k -= left_cnt;
u = node[u].rc;
v = node[v].rc;
lo = mid;
}
}
return lo;
}
int main() {
ll n, m;
if (scanf("%lld %lld", &n, &m) != 2) return 0; // 空输入安全返回
std::vector<ll> a(n);
for (ll i = 0; i < n; i++) {
scanf("%lld", &a[i]);
}
vals = a;
std::sort(vals.begin(), vals.end());
vals.erase(std::unique(vals.begin(), vals.end()), vals.end());
ll kinds = (ll)vals.size();
roots.assign(n + 1, 0);
for (ll i = 1; i <= n; i++) {
ll pos = std::lower_bound(vals.begin(), vals.end(), a[i - 1]) - vals.begin();
roots[i] = insert(roots[i - 1], 0, kinds, pos);
}
for (ll q = 0; q < m; q++) {
ll l, r, k;
scanf("%lld %lld %lld", &l, &r, &k); // 区间 [l,r] 的第 k 小 = 前缀 r 减前缀 l-1
printf("%lld\n", vals[kth(roots[l - 1], roots[r], k, 0, kinds)]);
}
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-07-05 21:47
# update_at: 2026-07-05 21:47
import sys
# 主席树用三个平行数组存全部节点:下标即节点编号,0 号是"空节点"哨兵
lc: list[int] = [0] # 左儿子编号
rc: list[int] = [0] # 右儿子编号
cnt: list[int] = [0] # 子树内数的个数
roots: list[int] = [0] # roots[i] = 插入前 i 个数后的树根
def insert(prev: int, lo: int, hi: int, pos: int) -> int:
"""在 prev 版本上把离散化下标 pos 的计数 +1,返回新版本节点编号。"""
node = len(cnt)
lc.append(lc[prev])
rc.append(rc[prev])
cnt.append(cnt[prev] + 1)
if hi - lo > 1:
mid = (lo + hi) // 2
if pos < mid:
lc[node] = insert(lc[prev], lo, mid, pos)
else:
rc[node] = insert(rc[prev], mid, hi, pos)
return node
def kth(u: int, v: int, k: int, lo: int, hi: int) -> int:
"""在值域 [lo,hi) 上,求版本区间 (u,v](第 v 个前缀减第 u 个前缀)的第 k 小的离散化下标。"""
while hi - lo > 1:
mid = (lo + hi) // 2
left_cnt = cnt[lc[v]] - cnt[lc[u]] # 值落在左半区的个数
if k <= left_cnt:
u, v, hi = lc[u], lc[v], mid
else:
k -= left_cnt
u, v, lo = rc[u], rc[v], mid
return lo
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n, m = next(data), next(data)
a = [next(data) for _ in range(n)]
# 离散化:值域压到 [0, len(vals))
vals = sorted(set(a))
rk = {v: i for i, v in enumerate(vals)}
# 每个前缀建一个版本,共 n 次插入、每次 O(log n) 个新节点
for x in a:
roots.append(insert(roots[-1], 0, len(vals), rk[x]))
out: list[str] = []
for _ in range(m):
l, r, k = next(data), next(data), next(data)
# 区间 [l,r] 的第 k 小 = 前缀 r 减前缀 l-1 后的第 k 小
out.append(str(vals[kth(roots[l - 1], roots[r], k, 0, len(vals))]))
print('\n'.join(out))
if __name__ == "__main__":
solve()复杂度
- 预处理:
次插入,每次新建一条 的链,时间与空间都是 。 - 每次询问:两棵树同步下降一层只做一次减法,
。 - 总计
,与数据范围匹配: 远小于朴素做法。
总结
- 静态区间第
小的经典模型:前缀可减性 + 可持久化权值线段树。 - 两个关键转化:用"前缀之差"消去区间维度,用"路径复制"消去版本维度,两者共同把
的空间压到 。 - 实现要点:先离散化把值域压到
;主席树用平行数组存节点,下标即节点编号, 号节点兼任"空节点"哨兵,省去所有空指针判断。
图示解析
主席树版本共享
下图展示样例中依次插入 1, 5, 2 后,第 3 棵树(root[3])如何与第 2 棵树共享大部分节点,只新建一条从根到值 2 叶子的链:
graph TD
subgraph root2["root[2]:已插入 1, 5"]
A2["[1..7) cnt=2"] --> B2["[1..4) cnt=1"]
A2 --> C2["[4..7) cnt=1"]
B2 --> D2["[1..2) cnt=1"]
B2 --> E2["[2..4) cnt=0"]
C2 --> F2["[4..6) cnt=1"]
C2 --> G2["[6..7) cnt=0"]
end
subgraph root3["root[3]:再插入 2(新建 3 个节点,加粗)"]
A3["[1..7) cnt=3"] --> B3["[1..4) cnt=2"]
A3 --> C3["[4..7) cnt=1(旧)"]
B3 --> D3["[1..2) cnt=1(旧)"]
B3 --> E3["[2..4) cnt=1"]
C3 -.复用 root2 的节点.-> C2
D3 -.复用.-> D2
end
观察重点:插入值 2 只影响从根到叶子 [2..4) 的路径,所以 root[3] 只有 3 个新节点(根、[1..4)、[2..4));右子树 [4..7) 和左深处的 [1..2) 都直接指回 root[2] 的旧节点。