替罪羊树与 KDTree

前言
替罪羊树是一种平衡树。对于平衡树的二叉搜索树性质,已经在这个博客比较详细地提到过了。那篇博客内,详细讲了的平衡树是 Splay。注意到 Splay 维护平衡的时候会直接通过旋转改变树的结构,在写平衡树套什么东西的时候,Splay 维护起来可能会有写吃力。因此这里重点写一下替罪羊树。
KD-Tree 和替罪羊树维护树平衡的方法一样,因此也可以提出来讲讲。
其实重点是 KDT 不是替罪羊。
替罪羊树
平衡树的插入,删除等操作基本都是一致的,重要的是如何维护树的平衡。例如给出一个不太平衡的树:
虽然单次查询不至于退化成线性,但是相比于正常情况的 也够慢了。考虑不平衡的两个典型,就是 和 的两个子树。
替罪羊树的思想很简单,现在既然这两个地方不平衡,那我直接把这两个子树重建一下就可以了。
平衡多了。但是众所周知,我不能每次修改一下就要全部重新把不平衡的地方重建。为了保证复杂度,应该需要衡量一个平衡因子 。若一个点的左右子树中,较大的子树大小占比达到了 ,就需要给子树重构。控制合适的 就可以控制替罪羊树的复杂度和常数。
一般来说 。显然不能直接取 。因为重构的复杂度是不可忽略的。如果有一点点不平衡就重构,虽然树会很平衡,但是多次重构会使其复杂度仍然接近 。
具体的重构实现,可以先把要重构的子树“拍扁”,即按照中序遍历得到对应的序列,然后对于这个序列重新构建子树即可。每次重新构建的时候,应该以中位数对应的节点当作根节点,因为前面是在替罪羊树上中序遍历得到的序列,已经有序了,直接取中间位置就是中位数。
和 Splay 不同的地方还有替罪羊的删除操作。替罪羊树并不能很好地直接删除,因此通常以打标记的方法,如果这个数字被删掉了(出现次数为零),在下一次重构的时候碰到这个点就不管它了。如果写的简单一点也可以直接忽视删除操作,保留原节点,大部分情况都不会被卡。
具体细节可以参考如下代码:
1 | struct SheepTree{ |
KDTree
建树
KD-Tree 是一个优雅的暴力,复杂度相对可观,但是比较裸的 KD-Tree 容易被卡常。来看一下这样一个题目:
在线地支持两个在平面直角坐标系上的操作:插入一个坐标为 ,权为 的点。矩形查点权和。
,空间限制 20MB。
强制在线卡掉 CDQ,空间限制卡掉树套树。因此我们就需要一个更加优秀的数据结构,可以很好地处理这个问题。KD-Tree 就是一个不错的选择。我们知道平衡树可以做到在一维(序列)的空间上对点进行很多高级的操作,KD-Tree 就是针对 K 维空间进行的,以二维为例子:
如果是在一个一维序列上,我们就会遵循 BST 的性质,每次取当前区间的中位数作为根,然后递归地建树。这样建出来的树一定具有 SBT 性质,随后怎么在加点删点时维护平衡就是它自己的事情了。
如果能找到一个标准,使得平面上的点也能建在一个树上,然后尽可能地满足 SBT 性质,那么复杂度就是接近 级别的。
KD-Tree 并不把事情搞的很复杂,每次我们只需要选择一个维度,然后在当前区间内找到中位数位置划分即可。当然每次选择维度不宜都是同一个,不然非常容易退化,常见的方法是交替选维度。
例如以 为标准,选择 :
然后换一个维度,各找中位数。注意到 在一个线上,为了方便随便选一个。假设选的是 :
如此类推划分:
然后按照划分的顺序,假设对于 为标准的时候,左侧为左子树, 时上面为左子树,则有这样一个树:
我们发现这个树看起来挺平衡的,那就可以根据这个关系来建树就行,控制了随机数据下在树上进行的操作复杂度约为 级。
重构
这样就出现了一个问题,因为加点的时候一定是加在一个叶子上的,如果我一直往一个区域加一堆点,这个子树的根就不一定是原来的中位数了。然后加着加着这个树就不平衡了,复杂度就退化了。
只需要和替罪羊树一样,搞上一个平衡因子,如果不平衡就拆下来重构。复杂度还是可以稳定在 。
然后到这里上面那个例题就可以做出来了,我们再维护一下一个点子树内的点权和,这个点本身的点权,和子数内所有点构成的最大矩形。询问的时候,如果询问矩形和这个点涵盖的矩形相离,不会造成贡献,如果询问区间包含了这个点涵盖的矩形,则直接返回这个点的子树点权和。
实测最慢的点不超过 2s。事实上,交替选点的复杂度一般视为单次树上操作 级别的。
估价
假设把求矩形内点权和,改成查询离一个点的曼哈顿距离最近的点的距离。
我们发现上面的划分方法有一个很大的弊端,就是有可能一个点和另一个点很近,但是在树上并不出在相邻的位置。举个例子,上面那个划分的图, 最近的点是 ,但是 不相邻。
因此每次询问的时候,就只能暴力跑一遍整个树吗?那我还建这个树干嘛。因此需要考虑怎么进行优化。
如果当前我找到的这个点,它的能涵盖到的矩形,距离询问点的距离还大于等于当前找到的最优答案,那还要往这个点的子树找干什么。
这样的看起来显然的优化实际上可以让复杂度变得非常优秀。确定好一个合适的搜索顺序后,基本可以看为也是根号级别的。这就是 KD-Tree 的估价。这里给出一个例子:
1 | inline int check(int x,int i){ // 估价函数 |
例题
BZOJ3489 A simple rmq problem
给定一个序列,在线处理若干询问,每次询问查询区间内只出现一次的数的最大值。
。
题解
先求出每个数字上一次出现的位置和后一次出现的位置,问题转化为找到最大的一个数,使得其上一次出现位置在左端点前,下一次出现位置在右端点后。那么不妨以这个点的位置、这个数上次出现位置、这个数字下一次出现位置三个维度为坐标,值为点权,建立一个 3D-Tree。每次查询的时候在 3D-Tree 上找到符合条件的点的点权最大值即可。
1 |
|
BZOJ4154 Generating Synergy
对于一个有根带点权树,支持两个操作:子树修改距根不超过 的点的点权;单点查询点权。
。
题解
二维线段树显然可以做这个题,现在考虑如何用 KDTree 解决。
如果我们求出树上每个点的深度和其对应子树的 dfn 区间,记为 。
则子树修改等价于修改满足 的点 的权值。将 看成横纵坐标,单次修改就相当于矩形内所有点赋值、单点查寻。因为不需要动态地加点删点,只需要给 KD-Tree 搞上一个区间赋值的下传标记即可实现。
1 |
|
- 标题: 替罪羊树与 KDTree
- 作者: Arahc
- 创建于 : 2021-11-25 08:00:00
- 更新于 : 2023-03-18 13:27:58
- 链接: https://arahc.github.io/2021/11/25/【笔记】替罪羊树与-KDTree/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。