数据结构: 权值线段树——线段树系列(提供模板)

前言:普通线段树 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=4Ask(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. 收集所有可能用到的数字(插入值、查询值)
  2. 排序 + 去重,建立映射:原数值 → 排名(1~size)
  3. 排名值域 [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);    
    }  
};
相关推荐
AI软著研究员7 小时前
程序员必看:软著不是“面子工程”,是代码的“法律保险”
算法
FunnySaltyFish7 小时前
什么?Compose 把 GapBuffer 换成了 LinkBuffer?
算法·kotlin·android jetpack
颜酱8 小时前
理解二叉树最近公共祖先(LCA):从基础到变种解析
javascript·后端·算法
地平线开发者1 天前
SparseDrive 模型导出与性能优化实战
算法·自动驾驶
董董灿是个攻城狮1 天前
大模型连载2:初步认识 tokenizer 的过程
算法
地平线开发者1 天前
地平线 VP 接口工程实践(一):hbVPRoiResize 接口功能、使用约束与典型问题总结
算法·自动驾驶
罗西的思考1 天前
AI Agent框架探秘:拆解 OpenHands(10)--- Runtime
人工智能·算法·机器学习
HXhlx1 天前
CART决策树基本原理
算法·机器学习
Wect1 天前
LeetCode 210. 课程表 II 题解:Kahn算法+DFS 双解法精讲
前端·算法·typescript
颜酱1 天前
单调队列:滑动窗口极值问题的最优解(通用模板版)
javascript·后端·算法