1. 算法简介
KD-tree(K-Dimensional),是一种对k维空间中的实例点进行存储以便对其进行快速检索的树形数据结构。 主要应用于多维空间关键数据的搜索。
KD-tree 的本质是一棵平衡树,将空间内的区域划分为一个超长方体,然后存储为节点进行维护。
以下为一个 \(k=2\) 时的 KD-tree。
2. 算法理论
考虑如何维护一个二维的平面信息。
将其分为多个区域,每次递归统计该区域内的子区域信息,进行维护。但是这样的做法过于劣。
那每次取中位点进行划分呢:考虑每次取横坐标的中位数所在的点作为节点,再依次递归到左右区间。这样可以使维护的代价更有优一些。
那每次两个维度交换划分呢:考虑每次按照横坐标、纵坐标、横坐标 \(\dots\) 这样的顺序进行划分,这样每次划分的区间会越来越小,维护的代价自然比划分一维的代价更优。
所以我们便知晓了 KD-tree 的基本原理:利用 \(k\) 维的交替划分,使得每一个子矩阵的信息能用一个或多个点进行维护。这样就能再保证时间复杂度等各项性能最优的情况下,维护一个二维平面的信息了。
3. 算法实现
3.1 建树
根据 2. 算法理论 所述,我们知晓了 KD-tree 的基本建树原理。
首先,我们可以定义一个结构体来存储 KD-tree 上的节点。
struct KD_tree {int x[2], v;int l, r, sum;int L[2], R[2];
} t[N];
这里 \(x\) 数组存储节点坐标,\(l,r\) 表示该节点在树上的左右儿子编号,\(v, sum\) 分别表示该节点的值以及该节点所管辖的子矩阵的信息。\(L,R\) 数组分别表示该节点所管辖的子矩阵的左下角、右上角的坐标。
将需要建成 KD-tree 的节点编号存入 \(b\) 数组中,并按以下顺序进行建树:
- 找出当前序列 \([l,r]\) 中 \(x/y\) 坐标的中位数所对应的节点,并将其存入当前节点。
- 取区间中点 \(mid\),分治处理 \([l,mid-1]\) 与 \([mid+1,r]\)。
注意,这里需要注意区间开闭的问题,在 KD-tree 中,子区间通常分为\([l,mid-1]\) 与 \([mid+1,r]\)。所以树上的一个节点对应的便是二维平面上的某一点。
如何快速取到中位数呢?这里可以使用C++库种自带的 nth_element
函数,它能找出序列 \(a\) 中第 \(k\) 大的数,并放置在位置 \(x\) 上,同时让小于 \(x\) 的所有位置上的数都小于等于 \(a_x\),大于 \(x\) 的所有位置上的数都大于等于 \(a_x\)。
然后运用运用线段数的建树思想,按照以上步骤构建一棵 KD-tree 即可。
Code:
int build(int l, int r, int D = 0) {//D=0/1 表示当前维度为 x/yint mid = l + r >> 1;nth_element(b + l, b + mid, b + r + 1, [D](int x, int y){return t[x].x[D] < t[y].x[D];});//找中位数并按要求组合顺序int x = b[mid];//存入该节点if(l < mid) t[x].l = build(l, mid - 1, D ^ 1);//递归左儿子 [l,mid-1]if(r > mid) t[x].r = build(mid + 1, r, D ^ 1);//递归左儿子 [mid+1,r]pushup(x);//信息上传return x;
}
注意: nth_element
适用范围必须在当前区间 \([l,r]\) 内。
以下以 luoguP4148 简单题 为例。
3.2 矩形操作
3.2.1 信息上传(pushup)
考虑一个节点在二维平面上所管辖的矩阵。
picture
可以发现,当前点所管瞎的矩阵:
- 左下角坐标为该点子树内所有点的横纵坐标最小值所组成。
- 左下角坐标为该点子树内所有点的横纵坐标最小值所组成。