K-D Tree 学习笔记
Dementor
·
·
题解
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)
我们在这个函数里,需要实现:
-
构建一颗由编号 l..r 节点 K-D Tree
-
构建好节点之间的父子关系
声明:
核心思想:根据某一维度进行划分
我们考虑 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 解决部分这类问题可能可以,但是大多数情况下不会作为正解出现