K-D Tree 学习笔记

· · 题解

K-D Tree

K-D Tree 是一种具备 BST 形态的维护 k 维空间上点的信息的一种数据结构

其名称 “K-D” 也是取 k 维空间这个意义

在节点数 n >> 2^k 时, K-D Tree 的运行效率优秀

单次操作,最坏时间复杂度是 O(n^{1-\frac{1}{k}})

思维清晰、代码清新的 K-D Tree 在这样的自身情况下不失为一个良好的解决问题的工具,有的题目甚至需要依赖 K-D Tree 才能解决,而有的题目则可以利用其取得一定部分分

建树

K-D Tree 维护的节点的位置、权值信息在绝大多数情况下都由题目给出,所以在下面的实现中,我们节点的编号和读入顺序一致,需要额外将读入的数据存储下来

下面所说 “编号” 统一先行指明是读入顺序编号

K-D Tree 的空间复杂度是 O(n)

int build (int l, int r)

我们在这个函数里,需要实现:

声明:

核心思想:根据某一维度进行划分

我们考虑 d 表示当前节点 按照 d 这一维度进行划分 ,具体地, d 这一维度上,小于等于当前节点的节点作为其左子树,大于当前节点的节点作为其右子树,再分别递归左右两边进行构建

方差优化

可以看出,以上构建方法保证了 K-D Tree 具有 BST 的性质,但是并没有保证时间复杂度

我们接下来提出两个优化方案,以保证 K-D Tree 具有正确的复杂度

首先是 方差优化

我们计算当前 l..r 的节点分别在 k 个维度上坐标的方差,取方差最大的一维作为当前节点的 d

中位数优化

取得 d 后,我们不任意取当前节点,而是将 d 维上坐标的中位数节点作为当前节点

由以上两个优化,我们就可以保证 K-D Tree 的高度最高是 O(n^{1-\frac{1}{k}}) 的,由此做到单次操作复杂度正确

建树部分到此实现

插入

void ins (int & x, int v)

插入编号为 v 的节点,当前在 K-D Tree 的 x 号节点

我们查看 d[x] ,按照 d[x] 这一维比较,如果小于等于 t[x][d[x]] 就递归左子树,否则递归右子树

## 删除 需要删除一个节点时,我们仿照 `ins` 先找到该节点,然后打上一个 $deltag$ ,等到我们发现一颗子树里 $deltag$ 的数目占比超过一个阈值时,我们对整颗子树直接重构,以此实现删除操作 类似线段树的 $lazytag$ 操作 ## 重构 首先,我们定义一个阈值 $REVAL$ 表示: + 如果某种比例超过了 $REVAL$ ,那么进行重构操作 一般来说, $REVAL$ 并不需要太小,定在 $0.7 \sim 0.8$ 就可以了 ### 一般重构 我们如果发现一个节点的某个子树在整颗子树种占比超过了 $REVAL$ ,那么重构 `void rebuild (int & x)` 首先,将子树压成序列,只需遍历整颗子树,存储节点编号进入一个 $g$ 数组即可 然后,假设最终发现子树里有 $tot$ 个节点,直接 `build (1, tot)` 即可,注意此时 `build` 函数内部在需要调用编号的时候都是用 `g[i]` 代替 `i` ### 删除重构 与一般重构类似地,只不过我们特殊判断一下有 $deltag$ 的节点不加入 $g$ 即可 ## 领域查询 这里进行的定义只是自己为了方便叙述所做出的 `#define` 一个节点的 **领域** 即整个以其为根的子树所被包含的超长方体 所以,我们对每个节点维护 $trge[d][0/1]$ 表示 $d$ 维上最小最大值是多少,以此维护出子树的超长方体 这个在 `pushup` 中容易维护 由此,我们也能看出, K-D Tree 具有维护区间修改的能力,具有类似线段树的性质 查询领域时,我们看一下询问领域与当前节点领域是否相离、内含,根据不同情况作出不同应对即可 这部分十分类似线段树 至此,我们了解了 K-D Tree 的基本操作,并且我们发现了 K-D Tree 和 BST 以及 SegmentTree 的相似之处 ## 习题 首先是可以视为模板题的 [P4148](https://www.luogu.com.cn/problem/P4148) 单点加、领域查, 2-D Tree 模板 也可以用其他的数据结构做,不对 K-D Tree 作尤其限制 上文的解释已经足以解决本题,于是放上代码便利对上文的理解 ```cpp #include <algorithm> #include <cstdio> #include <iostream> #include <math.h> using namespace std; const int N = 200010; int n, op, x[N], y[N], xf, yf, xs, ys, v[N], cur; int siz[N], sum[N], d[N], lc[N], rc[N], L[N], R[N], U[N], D[N]; int g[N], tot, lst, RT; double REVAL = 0.725; int min (int x, int y) {return (x > y) ? y : x;} int max (int x, int y) {return (x > y) ? x : y;} bool cmp1 (int p, int q) {return x[p] < x[q];} bool cmp2 (int p, int q) {return y[p] < y[q];} void pushup (int o) { siz[o] = siz[lc[o]] + siz[rc[o]] + 1; sum[o] = sum[lc[o]] + sum[rc[o]] + v[o]; L[o] = R[o] = x[o]; U[o] = D[o] = y[o]; if (lc[o]) { L[o] = min (L[o], L[lc[o]]), R[o] = max (R[o], R[lc[o]]); D[o] = min (D[o], D[lc[o]]), U[o] = max (U[o], U[lc[o]]); } if (rc[o]) { L[o] = min (L[o], L[rc[o]]), R[o] = max (R[o], R[rc[o]]); D[o] = min (D[o], D[rc[o]]), U[o] = max (U[o], U[rc[o]]); } } int build (int l, int r) { if (l > r) return 0; int mid = (l + r) >> 1; double a1 = 0, a2 = 0, v1 = 0, v2 = 0; for (int i=l;i<=r;i++) a1 += x[g[i]], a2 += y[g[i]]; a1 /= 1.0 * (r - l + 1); a2 /= 1.0 * (r - l + 1); for (int i=l;i<=r;i++) v1 += pow (x[g[i]] - a1, 2), v2 += pow (y[g[i]] - a2, 2); if (v1 > v2) nth_element (g + l, g + mid, g + r + 1, cmp1), d[g[mid]] = 1; else nth_element (g + l, g + mid, g + r + 1, cmp2), d[g[mid]] = 2; lc[g[mid]] = build (l, mid - 1); rc[g[mid]] = build (mid + 1, r); pushup (g[mid]); return g[mid]; } void plain (int o) { if (! o) return ; plain (lc[o]); g[++ tot] = o; plain (rc[o]); } void rebuild (int & o) { tot = 0; plain (o); o = build (1, tot); } bool bad (int o) { return REVAL * siz[o] <= (double) max (siz[lc[o]], siz[rc[o]]); } void ins (int & o, int v) { if (! o) { o = v; pushup (o); return ; } if (d[o] == 1) { if (x[v] <= x[o]) ins (lc[o], v); else ins (rc[o], v); } else { if (y[v] <= y[o]) ins (lc[o], v); else ins (rc[o], v); } pushup (o); if (bad (o)) rebuild (o); } bool out (int o) { if (L[o] > xs) return 1; if (R[o] < xf) return 1; if (D[o] > ys) return 1; if (U[o] < yf) return 1; return 0; } bool in (int o) { if (L[o] >= xf && R[o] <= xs && D[o] >= yf && U[o] <= ys) return 1; return 0; } int que (int o) { if (! o || out (o)) return 0; if (in (o)) return sum[o]; int ret = 0; if (x[o] >= xf && x[o] <= xs && y[o] >= yf && y[o] <= ys) ret += v[o]; return que (lc[o]) + que (rc[o]) + ret; } template <typename T> void read (T & x) { x = 0; char c = getchar (); while (c < 48 || c > 57) c = getchar (); while (c >= 48 && c <= 57) x = (x << 1) + (x << 3) + (c ^ 48), c = getchar (); } void write (int x) { if (x > 9) write (x / 10); putchar (x % 10 + 48); } int main () { read (n); while (233) { read (op); if (op == 3) break ; if (op == 1) { cur ++; read (x[cur]); read (y[cur]); read (v[cur]); x[cur] ^= lst; y[cur] ^= lst; v[cur] ^= lst; ins (RT, cur); } else { read (xf); read (yf); read (xs); read (ys); xf ^= lst; yf ^= lst; xs ^= lst; ys ^= lst; if (xf > xs) swap (xf, xs); if (yf > ys) swap (yf, ys); lst = que (RT); write (lst); putchar ('\n'); } } } ``` 本题不需要特殊卡常,认真书写即可 ## 注意事项 包括 oi-wiki 在内许多地方都有提到 K-D Tree 查询最近最远点 但实际上 K-D Tree 的结构确实没有适应这类问题 而且这类问题已经有另外的做法体系 所以这里直接略去这一部分的学习与笔记,并且做出提示: K-D Tree 解决部分这类问题可能可以,但是大多数情况下不会作为正解出现