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

前言:普通线段树 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);    
    }  
};
相关推荐
笨手笨脚の2 小时前
链表与LinkedList
java·数据结构·链表·linkedlist
历程里程碑2 小时前
破解三数之和:双指针高效解法
c语言·数据结构·c++·经验分享·算法·leetcode·排序算法
Vect__2 小时前
25.12.27 算法日记——双指针
c++·算法
Swizard2 小时前
数据不够代码凑?用 Albumentations 让你的 AI 模型“看”得更广,训练快 10 倍!
python·算法·ai·训练
AI题库2 小时前
NLTK自然语言处理实战:1.3 NLTK核心数据结构
数据结构·人工智能·自然语言处理
一个专注写代码的程序媛2 小时前
流式读取数据
java·数据结构·算法
Halo_tjn2 小时前
Java Set集合知识点
java·开发语言·数据结构·windows·算法
-Xie-2 小时前
Redis(十六)——底层数据结构(一)
java·数据结构·redis
小园子的小菜2 小时前
深入理解Trie树:敏感词过滤的核心原理与实现思路
算法