[算法学习]——可持久化权值线段树求区间第k小(主席树)

前置知识

树状数组,线段树 (本文不讲)

权值线段树,树上二分,动态开点线段树(下文会讲)

前置知识讲解

权值线段树

虽然叫线段树,但事实上我们一般用树状数组去写权值线段树

并非像线段树那样维护数组下标区间 [ l , r ] [l, r] [l,r]中的信息,而是以权值为下标,维护权值为 [ l , r ] [l, r] [l,r]的数量

一个简单的例子:询问数组中有多少满足 5 ≤ x ≤ 8 5 \le x \le 8 5x8的权值 x x x

首先我们考虑如何维护权值线段树,对于第 i i i个元素 a i a_i ai,我们应该 b i t . m o d i f y ( a i , 1 ) bit.modify(a_i, 1) bit.modify(ai,1),表示权值为 a i a_i ai的数量 + 1 +1 +1

考虑查询,树状数组是类似于前缀和的思想, b i t . q u e r y ( x ) bit.query(x) bit.query(x)表示权值最多为 x x x的数量,那么就是 b i t . q u e r y ( 8 ) − b i t . q u e r y ( 4 ) bit.query(8) - bit.query(4) bit.query(8)bit.query(4),值最多为 8 8 8的数量减去值最多为 4 4 4的数量,那么我们就得到了权值处于区间 [ 5 , 8 ] [5,8] [5,8]的数量

我们可以画个图简单的理解下:

image-20240926205919335

在这个图中,表示数组有两个权值为一的元素,一个权值为二的元素,零个权值为四的元素,然后我们利用树状数组查询 b i t . q u e r y ( 4 ) bit.query(4) bit.query(4)相当于把所有小于等于 4 4 4的值的数量都求出来了,即 b i t . q u e r y ( 4 ) = 2 + 1 + 2 + 0 = 5 bit.query(4) = 2 + 1 + 2 + 0 = 5 bit.query(4)=2+1+2+0=5

注:下文的主席树还是以用线段树去实现权值线段树,这里主要是讲思想,所以用了树状数组

树上二分

学会了权值线段树后,我们考虑求全局第 k k k小,我们二分一个答案 m i d mid mid,然后计算权值处于区间 [ l , m i d ] [l, mid] [l,mid]的数量 c n t cnt cnt

如果 c n t < k cnt < k cnt<k的话,那么显然答案不会在 [ l , m i d ] [l, mid] [l,mid]区间上,所以我们要判断区间 [ m i d + 1 , r ] [mid + 1, r] [mid+1,r]中哪个数有可能是答案,此外,因为 [ l , m i d ] [l, mid] [l,mid]都不是答案,所以 k k k要减去这些数量,即 k = k − c n t k = k - cnt k=kcnt

如果 c n t ≥ k cnt \ge k cntk的话,说明答案在 [ l , m i d ] [l, mid] [l,mid]区间内,那么我们继续迭代判断即可。

我们可以手动模拟一下:

image-20240926210230954

例如我们在这个权值线段树上求全局第 3 3 3

一开始 l = 1 , r = 6 , m i d = l + r 2 = 3 l = 1, r = 6, mid = \frac {l + r} 2 = 3 l=1,r=6,mid

### 可持久化线段树解决带区间修改的区间第K大问题 对于带有区间修改功能的可持久化线段树区间第K大问题,主要思路在于构建并维护一棵能够反映历史版本变化的线段树结构。这棵不仅支持查询操作还允许对特定范围内的元素进行批量更新。 #### 构建初始数据结构 为了处理此类问题,首先需要准备输入数列表,并对其进行预处理——即排序与去重操作[^1]: ```cpp vector<int> nums; // 对nums执行sort和unique算法去除重复项... auto end_unique = unique(nums.begin(), nums.end()); nums.erase(end_unique, nums.end()); ``` 上述代码片段展示了如何利用STL库函数`std::sort()`以及`std::unique()`完成这一过程。经过此步之后得到的是不含任何冗余信息的独特整数集合,这些将成为后续建立线段树的基础。 #### 创建基础版可持久化线段树 接下来创建一个空的线段树实例作为起点,在此基础上逐步加入原始序列中的各个元素形成新的版本。每当向其中添加一个新的数字时,实际上是在原有基础上复制出一条新路径而非直接更改旧有节点的数据;这样做可以确保每次变更都保留下来供将来回溯使用。 当面对具有区间修改需的情况时,则需进一步扩展基本模型使之具备高效地处理连续范围内多个位置同步变动的能力。一种常见做法是采用懒惰传播(Lazy Propagation)机制配合动态开点策略减少不必要的内存消耗[^2]。 #### 实现懒惰标记下的区间修改 针对每一个可能被影响到的时间戳版本v及其对应子root[v],引入额外字段lazy[]用来存储尚未向下传递给子孙结点的信息量差额。具体来说就是在遇到涉及某一段落[l,r]内所有成员统一增减相同偏移量delta的情形下,只需简单设置相应根部节点处的标志位即可暂时搁置实际改动动作直到真正访问该部分为止。 以下是简化后的C++伪码表示法展示怎样在已有框架之上增加这项特性: ```cpp struct Node { int sum; // 当前区间总和 int lazy; // 待应用至子节点的变化增量 }; void push_down(int v, int tl, int tr){ if (tl != tr && node[v].lazy != 0) { int tm = (tl + tr) / 2; apply(node[left_child(v)], node[v].lazy); apply(node[right_child(v)], node[v].lazy); node[v].lazy = 0; } } void update_range(int &pv, int pv_old, int l, int r, int tl, int tr, int delta){ if (!pv) { clone_node(pv, pv_old); } if (l > tr || r < tl) return; if (l <= tl && tr <= r){ apply(node[pv], delta); return ; } int tm = (tl + tr) >> 1; push_down(pv, tl, tr); update_range(left_child(pv), left_child(pv_old), l, r, tl, tm, delta); update_range(right_child(pv), right_child(pv_old), l, r, tm+1, tr, delta); pull_up(pv); } ``` 这里定义了一个辅助方法`push_down()`负责将父级累积下来的延迟调整分发下去,而核心逻辑则由`update_range()`承载,它接受当前正在编辑的新版本索引、上一时刻的状态指针连同待作用域边界参数共同决定何时何地实施累加运算。 #### 查询指定时间片上的第K大数据 最后一步便是编写专门用于检索任意给定时间段内部满足条件的最大几个候选对象之一的功能模块。考虑到已经建立了完整的多世代存档体系,因此只需要沿着选定的历史轨迹追溯回去找到匹配的目标即可得出结论。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值