前言:普通线段树 vs 权值线段树
在基础线段树中,我们处理的是数组下标上的区间操作 ------ 节点代表"位置区间",如 [1, n]。
但如果我们的问题是:
"在动态插入/删除数字的过程中,如何快速查询:
- 数
x的出现次数?- 小于
x的数有多少个?(即x的排名)- 第
k小的数是多少?"
这时,普通线段树就无能为力了,因为我们关心的不是"位置",而是"数值本身"。
权值线段树 应运而生 ------ 它将数值范围 作为线段树的区间,每个叶子代表一个可能的数值 ,节点存储该数值范围内的出现次数(权值)。
其三大核心操作:单点修改(插入/删除)、前缀和查询(排名)、第 k 小查询。
什么是权值线段树?
核心思想:以"值域"为下标,以"频率"为权值
- 普通线段树 :下标 = 数组索引(位置),值 =
arr[i] - 权值线段树 :下标 = 数值本身(值域),值 = 该数值的出现次数
本质上,权值线段树就是对"值域"建立的普通线段树,只不过每个位置的"权值"是频率。
权值线段树支持的核心操作
1. 插入/删除一个数(单点修改)
- 操作 :将数字
x的频率+1(插入)或-1(删除) - 实现 :在线段树的叶子
x处执行sum += delta - 复杂度 :O(log M),其中
M是值域大小(如1e5)
2. 查询排名(前缀和查询)
- 问题 :小于
x的数有多少个? → 即x的排名(从 1 开始) - 转换 :求值域区间
[1, x-1]的频率和 - 实现 :调用区间查询
Ask(1, 1, x-1)(函数具体实现见下方模板) - 排名 =
Ask(1, 1, x-1) + 1
例如:
x=4,Ask(1,1,3)=3→ 排名为 4(因为有 3 个数比它小)
3. 查询第 k 小的数(Kth 查询)
- 问题 :集合中第
k小的数是多少? - 原理 :利用线段树的二分性质
- 若左子树的
sum ≥ k,说明第k小在左半值域 - 否则,在右半值域,且找第
k - left.sum小
- 若左子树的
- 实现 :递归
Kth(p, k)(函数具体实现见下方模板) - 复杂度:O(log M)
模板中
Kth(p, k)正是此操作。
值域离散化:处理大范围数值
权值线段树的空间复杂度取决于值域大小 M 。若数值范围很大(如 1e9),但实际数字数量 n 很小(如 1e5),则需离散化(Coordinate Compression):
- 收集所有可能用到的数字(插入值、查询值)
- 排序 + 去重,建立映射:
原数值 → 排名(1~size) - 在排名值域
[1, size]上建权值线段树
离散化后,值域大小
M = n,空间O(n)
模板
cpp
template <int N>struct Segment_tree{
#define ls (p<<1)
#define rs ((p<<1)|1)
struct{
int l,r;
int sum;
}tr[4*N];
void push_up(int p){
tr[p].sum=tr[ls].sum+tr[rs].sum;
}
void Build(int p,int lo,int ro){ //建树,值域为lo~ro
tr[p].l=lo,tr[p].r=ro;
tr[p].sum=0;
if(lo==ro) return;
int mid=(lo+ro)>>1;
build(ls,lo,mid);
build(rs,mid+1,ro);
push_up(p);
}
void Fix(int p,int x,int k){ //数x增加/减少k
if(tr[p].l==tr[p].r){
tr[p].sum+=k;
return;
}
int ndmid=(tr[p].l+tr[p].r)>>1;
if(x<=ndmid) Fix(ls,x,k);
else Fix(rs,x,k);
push_up(p);
}
int Ask(int p,int lo,int ro){ //查询范围内有多少个数,可查询x的排名:Ask(1,1,x-1)+1
if(lo<=tr[p].l && ro>=tr[p].r) return tr[p].sum;
int res=0,ndmid=(tr[p].l+tr[p].r)>>1;
if(lo<=ndmid) res+=Ask(ls,lo,ro);
if(ro>ndmid) res+=Ask(rs,lo,ro);
return res;
}
int Kth(int p,int k){ //查询第k小的数
if(tr[p].l==tr[p].r) return tr[p].l;
if(k<=tr[ls].sum) return Kth(ls,k);
else return Kth(rs,k-tr[ls].sum);
}
};