题目情境#
题目链接:https://www.luogu.com.cn/problem/P3834 ↗
题目背景
这是个非常经典的可持久化权值线段树入门题——静态区间第 小。
题目描述
如题,给定 个整数构成的序列 ,将对于指定的闭区间 查询其区间内的第 小值。
输入格式
第一行包含两个整数,分别表示序列的长度 和查询的个数 。
第二行包含 个整数,第 个整数表示序列的第 个元素 。
接下来 行每行包含三个整数 , 表示查询区间 内的第 小值。
输出格式
对于每次询问,输出一行一个整数表示答案。
输入 #1
5 5
25957 6405 15770 26287 26465
2 2 1
3 4 1
4 5 1
1 2 2
4 4 1plaintext输出 #1
6405
15770
26287
25957
26287plaintext- 对于 的数据,满足 ,,,。
算法解析#
1. 每棵树维护什么?
普通线段树一般维护数组下标区间,而主席树这里维护的是值域。
struct node{
int l,r,s;
};cpp其中:
l:左儿子编号r:右儿子编号s:当前值域内有多少个数
例如:
[1,4] s=4
/ \
[1,2] s=2 [3,4] s=2text也就是说,它本质上是一棵统计每个值出现次数的线段树。
2. root[i] 是什么?
root[i]cpp表示:
前 i 个数形成的值域线段树。
例如:
a = [3,1,4,2]
root[0] = []
root[1] = [3]
root[2] = [3,1]
root[3] = [3,1,4]
root[4] = [3,1,4,2]text3. 怎么查询第 k 小?
查询:
query(root[l-1],root[r],1,bn,k);cpp核心代码:
int s=tr[lc(y)].s-tr[lc(x)].s;cpp这里:
x = root[l-1]
y = root[r]text所以 s 表示:
[l,r]中有多少个数落在当前值域的左半边。
不断对值域二分,直到找到答案。
4. 为什么要有很多版本?
每加入一个新数,就产生一个新版本:
insert(root[i-1],root[i],1,bn,id);cpp也就是:
root[0]
↓ 插入 a1
root[1]
↓ 插入 a2
root[2]
↓ 插入 a3
root[3]
...text但是不会把整棵树复制一遍。
5. 新版本是怎么产生的?
核心:
void insert(int x,int &y,int l,int r,int pos){
y=++idx;
tr[y]=tr[x];
tr[y].s++;
...
}cpp其中:
x = 旧版本节点
y = 新版本节点text最重要的是:
y=++idx; // 新建节点
tr[y]=tr[x]; // 复制旧节点
tr[y].s++; // 只修改新节点cpp也就是说:
旧节点不修改,先复制一份,再修改副本。
因此一次插入只增加:
O(log n)text个节点,而不是复制整棵树。
注意int &y,它可以让父节点的lc(y)自动附到对应的子节点。
总体复杂度:
建树:O(n log n)
单次查询:O(log n)
空间:O(n log n)textPython代码实现#
import bisect
class Node:
def __init__(self, l=0, r=0, s=0):
self.l = l
self.r = r
self.s = s
n, m = map(int, input().split())
a = list(map(int, input().split()))
a = [0] + a # 转换为1-based索引
# 离散化处理
sorted_b = sorted(a[1:])
unique_b = []
prev = None
for num in sorted_b:
if num != prev:
unique_b.append(num)
prev = num
bn = len(unique_b)
# 初始化主席树
tr = [Node(0, 0, 0)] # 空节点,索引0
idx = 1
root = [0] * (n + 1)
root[0] = 0
def insert(x, l, r, pos):
global idx
y = Node(tr[x].l, tr[x].r, tr[x].s + 1)
tr.append(y)
y_idx = idx
idx += 1
if l == r:
return y_idx
mid = (l + r) // 2
if pos <= mid:
new_l = insert(tr[x].l, l, mid, pos)
y.l = new_l
else:
new_r = insert(tr[x].r, mid + 1, r, pos)
y.r = new_r
return y_idx
# 构建主席树的每个版本
for i in range(1, n + 1):
num = a[i]
pos = bisect.bisect_left(unique_b, num) + 1 # 转换为1-based的id
root[i] = insert(root[i-1], 1, bn, pos)
# 查询函数
def query(x, y, l, r, k):
if l == r:
return l
mid = (l + r) // 2
left_x = tr[x].l
left_y = tr[y].l
s = tr[left_y].s - tr[left_x].s
if k <= s:
return query(left_x, left_y, l, mid, k)
else:
return query(tr[x].r, tr[y].r, mid + 1, r, k - s)
# 处理每个查询并输出结果
output = []
for _ in range(m):
l, r, k = map(int, input().split())
id = query(root[l-1], root[r], 1, bn, k)
output.append(str(unique_b[id-1])) # id转换为0-based索引
print('\n'.join(output))pythonC++代码实现#
// 主席树 O(nlognlogn)
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
#define N 200005
#define lc(x) tr[x].l
#define rc(x) tr[x].r
struct node{
int l,r,s; //s:节点值域中有多少个数
}tr[N*20];
int root[N],idx;
int n,m,a[N],b[N];
void insert(int x,int &y,int l,int r,int pos){
y=++idx; //开点
tr[y]=tr[x]; tr[y].s++;
if(l==r) return;
int m=l+r>>1;
if(pos<=m) insert(lc(x),lc(y),l,m,pos);
else insert(rc(x),rc(y),m+1,r,pos);
}
int query(int x,int y,int l,int r,int k){
if(l==r) return l;
int m=l+r>>1;
int s=tr[lc(y)].s-tr[lc(x)].s;
if(k<=s) return query(lc(x),lc(y),l,m,k);
else return query(rc(x),rc(y),m+1,r,k-s);
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1; i<=n; i++){
scanf("%d",&a[i]); b[i]=a[i];
}
sort(b+1,b+n+1);
int bn=unique(b+1,b+n+1)-b-1; //去重后的个数
for(int i=1; i<=n; i++){
int id=lower_bound(b+1,b+bn+1,a[i])-b;//下标
insert(root[i-1],root[i],1,bn,id);
}
while(m--){
int l,r,k; scanf("%d%d%d",&l,&r,&k);
int id=query(root[l-1],root[r],1,bn,k);
printf("%d\n",b[id]);
}
}c查询x是第几小的话,参考如下代码:
int query_rank(int x,int y,int l,int r,int pos){
if(l==r) return 1;
int m=l+r>>1;
if(pos<=m) return query_rank(lc(x),lc(y),l,m,pos);
int s=tr[lc(y)].s-tr[lc(x)].s;
return s+query_rank(rc(x),rc(y),m+1,r,pos);
}
int pos=lower_bound(b+1,b+bn+1,v)-b;
int rk=query_rank(root[l-1],root[r],1,bn,pos);
printf("%d\n",rk);c