KD 树 学习笔记
k-D Tree (KDT , k-Dimension Tree) 是一种可以 高效处理
在结点数
在算法竞赛的题目中,一般有
建树
k-D Tree 具有二叉搜索树的形态,二叉搜索树上的每个结点都对应
假设我们已经知道了
若当前超长方体中只有一个点,返回这个点。
选择一个维度,将当前超长方体按照这个维度分成两个超长方体。
选择切割点:在选择的维度上选择一个点,这一维度上的值小于这个点的归入一个超长方体(左子树
其余的归入另一个超长方体(右子树) , ) 。 将选择的点作为这棵子树的根节点,递归对分出的两个超长方体构建左右子树,维护子树的信息。
为了方便理解,我们举一个
其构建出 k-D Tree 的形态可能是这样的:
其中树上每个结点上的坐标是选择的分割点的坐标,非叶子结点旁的
这样的复杂度无法保证。对于
- 轮流选择
个维度,以保证在任意连续 层里每个维度都被切割到。 - 每次在维度上选择切割点时选择该维度上的 中位数,这样可以保证每次分成的左右子树大小尽量相等。
可以发现,使用优化
现在,构建 k-D Tree 时间复杂度的瓶颈在于快速选出一个维度上的中位数,并将在该维度上的值小于该中位数的置于中位数的左边,其余置于右边。如果每次都使用 sort
函数对该维度进行排序,时间复杂度是
我们来回顾一下快速排序的思想。每次我们选出一个数,将小于该数的置于该数的左边,大于该数的置于该数的右边,保证该数在排好序后正确的位置上,然后递归排序左侧和右侧的值。这样的期望复杂度是 algorithm
库中,有一个实现相同功能的函数 nth_element()
,要找到 s[l]
和 s[r]
之间的值按照排序规则 cmp
排序后在 s[mid]
位置上的值,并保证 s[mid]
左边的值小于 s[mid]
,右边的值大于 s[mid]
,只需写 nth_element(s+l,s+mid,s+r+1,cmp)
。
借助这种思想,构建 k-D Tree 时间复杂度是
高维空间上的操作
在查询高维矩形区域内的所有点的一些信息时,记录每个结点子树内每一维度上的坐标的最大值和最小值。如果当前子树对应的矩形与所求矩形没有交点,则不继续搜索其子树;如果当前子树对应的矩形完全包含在所求矩形内,返回当前子树内所有点的权值和;否则,判断当前点是否在所求矩形内,更新答案并递归在左右子树中查找答案。
int query(int p) {
if (!p)
return 0;
bool flag{false};
for (int k : {0, 1})
flag |= (!(l.x[k] <= t[p].L[k] && t[p].R[k] <= h.x[k]));
if (!flag)
return t[p].sum;
for (int k : {0, 1})
if (t[p].R[k] < l.x[k] || h.x[k] < t[p].L[k])
return 0;
int ans{0};
flag = false;
for (int k : {0, 1})
flag |= (!(l.x[k] <= t[p].x[k] && t[p].x[k] <= h.x[k]));
if (!flag)
ans = t[p].v;
return ans += query(t[p].l) + query(t[p].r);
}
复杂度分析
先考虑二维的,在查询矩形
- 与
无交。 - 完全被
包含。 - 部分被
包含。
显然单次查询的复杂度是第 3 类点的个数。注意到第三类点的矩形要么完全包含
首先,我们不妨令矩形的所有边偏移
注意到互不包含的第 3 类点所对应的矩形,一定有
考虑对于某一个结点
而因为建树的时候,每个点是其整个子树在当前划分维度上的中位数,所以子树大小必定减半。于是,设
由主定理得
将递归式推广到
插入 / 删除
如果维护的这个
NOTE
很多选手会使用替罪羊树结构来维护。但是注意到在刚才的复杂度分析中,要求儿子的子树大小严格减半,即树高必须为严格的
根号重构
插入的时候,先存下来要插入的点,每
删除打个标记即可。如果要求较为严格,可以维护树内有多少个被删除了,达到
修改复杂度均摊
二进制分组
考虑维护若干棵
插入的时候,新增一棵大小为
容易发现需要合并的树的大小一定从
查询的时候,直接分别在每棵树上查询,复杂度为