K-D Tree
1 引入
在
2 基本操作
2.1 建树
我们希望这棵
但是如果每一次都按照一维来排序的话,可能会出现所有点在这一维上都很接近,但是在别的维上相差很远的情况,复杂度会爆炸。那么我们就需要采取一个优化措施,我们每一次划分是轮流按照
现在的问题是找出区间中某一维度上的中位数的值,如果直接排序的话总复杂度是 nth_element
来简单实现,复杂度是
基础代码如下:
void build(int &p, int l, int r, int typ) {
if(l > r) return p = 0, void();
int mid = (l + r) >> 1;
nth_element(a + l, a + mid, a + r + 1, [typ](node x, node y){return x.v[typ] < y.v[typ];});//按照当前维排序,找中位数
p = ++tot;
t[p].v[0] = a[mid].v[0], t[p].v[1] = a[mid].v[1];//赋值
build(lp, l, mid - 1, typ ^ 1), build(rp, mid + 1, r, typ ^ 1);
}
那么此时我们分析一下
struct KD_Tree {
int l, r, v[2], mn[2], mx[2];
// 当前点坐标 坐标最小值 坐标最大值
}t[Maxn];
int tot = 0;
#define lp t[p].l
#define rp t[p].r
void pushup(int p) {
for(int i = 0; i < 2; i++) {
t[p].mn[i] = t[p].mx[i] = t[p].v[i];
if(lp) {
t[p].mn[i] = min(t[p].mn[i], t[lp].mn[i]);
t[p].mx[i] = max(t[p].mx[i], t[lp].mx[i]);
}
if(rp) {
t[p].mn[i] = min(t[p].mn[i], t[rp].mn[i]);
t[p].mx[i] = max(t[p].mx[i], t[rp].mx[i]);
}
}
}
void build(int &p, int l, int r, int typ) {
if(l > r) return p = 0, void();
int mid = (l + r) >> 1;
nth_element(a + l, a + mid, a + r + 1, [typ](node x, node y){return x.v[typ] < y.v[typ];});
p = ++tot;
t[p].v[0] = a[mid].v[0], t[p].v[1] = a[mid].v[1];
build(lp, l, mid - 1, typ ^ 1), build(rp, mid + 1, r, typ ^ 1);
pushup(p);//上传标记
}
2.2 插入与删除
如果我们维护的点集会发生变动,此时静态建树的
2.2.1 根号重构
我们可以想到的是利用替罪羊树的重构套路对
考虑另一种方式,设定一个阈值
如此,当我们取到
2.2.2 二进制分组
如果仅仅要求插入,那么这种做法是更优的。我们维护若干棵大小为
插入的时候,新增一棵大小为
这样做的总复杂度是均摊
代码如下:
namespace KDT {
//...
int tot = 0;
int trs[Maxn], top;//拍扁的时候会删除节点,可以用垃圾桶来节省空间
void del(int &p) {//删除节点
trs[++top] = p;
t[p] = {0, 0, 0, 0, 0, 0, 0, 0};
p = 0;
}
int newnode() {
return top ? trs[top--] : ++tot;
}
//...
void append(int &p) {//拍扁重构
if(!p) return ;
a[++cnt] = {t[p].v[0], t[p].v[1]};//记录下当前点
append(lp), append(rp);
del(p);//删除
}
//...
}
int main() {
//...
for(int i = 1; i <= n; i++) {
int x, y;
cin >> x >> y;
for(int j = 0; j < 20; j++) {//每一个根查询一边
KDT::query(rt[j], x, y);
}
a[cnt = 1] = {x, y};//开始重构
for(int j = 0; j < 20; j++) {
if(!rt[j]) {//当前大小为 2^j 的树还没有建,无法合并,在这里重建树
KDT::build(rt[j], 1, cnt, 0);
break;
}
else {
KDT::append(rt[j]);//拍平重构
}
}
}
//...
return 0;
}
3 查询操作
3.1 矩阵查询
我们在查询矩阵中所有点的信息时,按照传统的方式去进行递归。如果当前子树对应的矩形和目标矩形无交点,则不继续搜索;否则如果被目标矩形完全包含,直接返回整个子树的信息即可;否则先判断当前节点是否合法,然后再递归下去找答案。
可以证明,这样做的复杂度是单次
考虑将每个节点对应的矩阵分
类:
- 与目标矩阵无交点。
- 完全被目标矩阵包含。
- 与目标矩阵有部分交集。
显然前两种如果递归到我们会直接返回,所以只需要考虑第三种矩阵。而第三种矩阵又分为完全包含目标矩阵的部分和剩下的部分。前者显然最多只有
个。 现在考虑后者。我们对于一个节点来看,我们在它的儿子和孙子处分别对
坐标进行了一次划分,共划分为了 个子矩阵。考虑查询矩阵的每一条边,此时它经过了几个子矩阵,就代表它还要访问那些子树。显然可以发现,对于任意一条边来讲,它最多经过 个这样的子矩阵。 设当前子树大小为
,由于我们建树时保证了子树大小每一次减半,所以子矩阵大小应该是 的。于是有以下递归式: 根据主定理可知
。将其推广至 维可得递归式为 ,可得 。
3.2 邻域查询
邻域查询可以求出平面上一个点的最近 / 最远点。值得注意的是
假设现在我们要找出离当前点最近的点,我们暴力遍历
同理还可以进行最优性剪枝,如果当前节点的估价函数都比当前答案大,那么子树内不可能有更优的答案,直接返回即可。
4 例题
例 1 [SDOI2010] 捉迷藏
此题就是求最近最远点对的题目。暴力枚举每一个点,求出不是自己的离自己最近和最远的点的曼哈顿距离即可。
代码如下:
#include <bits/stdc++.h>
using namespace std;
const int Maxn = 2e5 + 5;
const int Inf = 2e9;
int n;
struct node {
int v[2];
}a[Maxn];
int rt;
int ans1 = Inf, ans2 = -Inf;
namespace KDT {
struct KD_Tree {
int l, r, v[2], mn[2], mx[2];
}t[Maxn];
int tot = 0;
#define lp t[p].l
#define rp t[p].r
void pushup(int p) {
for(int i = 0; i < 2; i++) {
t[p].mn[i] = t[p].mx[i] = t[p].v[i];
if(lp) {
t[p].mn[i] = min(t[p].mn[i], t[lp].mn[i]);
t[p].mx[i] = max(t[p].mx[i], t[lp].mx[i]);
}
if(rp) {
t[p].mn[i] = min(t[p].mn[i], t[rp].mn[i]);
t[p].mx[i] = max(t[p].mx[i], t[rp].mx[i]);
}
}
}
void build(int &p, int l, int r, int typ) {
if(l > r) return p = 0, void();
int mid = (l + r) >> 1;
nth_element(a + l, a + mid, a + r + 1, [typ](node x, node y){return x.v[typ] < y.v[typ];});
p = ++tot;
t[p].v[0] = a[mid].v[0], t[p].v[1] = a[mid].v[1];
build(lp, l, mid - 1, typ ^ 1), build(rp, mid + 1, r, typ ^ 1);
pushup(p);
}
int dis(int x1, int y1, int x2, int y2) {//距离
return abs(x1 - x2) + abs(y1 - y2);
}
int fmin(int p, int x, int y) {//最小值估价函数
int res = 0;
if(x < t[p].mn[0]) res += t[p].mn[0] - x;
if(x > t[p].mx[0]) res += x - t[p].mx[0];
if(y < t[p].mn[1]) res += t[p].mn[1] - y;
if(y > t[p].mx[1]) res += y - t[p].mx[1];
return res;
}
int fmax(int p, int x, int y) {//最大值估价函数
int res = 0;
res += max(abs(x - t[p].mn[0]), abs(x - t[p].mx[0]));
res += max(abs(y - t[p].mn[1]), abs(y - t[p].mx[1]));
return res;
}
void qmin(int p, int x, int y) {
if(!p) return;
if(!(x == t[p].v[0] && y == t[p].v[1])) ans1 = min(ans1, dis(x, y, t[p].v[0], t[p].v[1]));//注意不能是自己本身
int vl = Inf, vr = Inf;
if(lp) vl = fmin(lp, x, y);
if(rp) vr = fmin(rp, x, y);
if(vl < vr) {//启发式搜索,先搜更小的
if(vl < ans1) qmin(lp, x, y);
if(vr < ans1) qmin(rp, x, y);
}
else {
if(vr < ans1) qmin(rp, x, y);
if(vl < ans1) qmin(lp, x, y);
}
}
void qmax(int p, int x, int y) {
if(!p) return ;
ans2 = max(ans2, dis(x, y, t[p].v[0], t[p].v[1]));
int vl = -Inf, vr = -Inf;
if(lp) vl = fmax(lp, x, y);
if(rp) vr = fmax(rp, x, y);
if(vl > vr) {
if(vl > ans2) qmax(lp, x, y);
if(vr > ans2) qmax(rp, x, y);
}
else {
if(vr > ans2) qmax(rp, x, y);
if(vl > ans2) qmax(lp, x, y);
}
}
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n;
for(int i = 1; i <= n; i++) {
cin >> a[i].v[0] >> a[i].v[1];
}
KDT::build(rt, 1, n, 0);
int ans = Inf;
for(int i = 1; i <= n; i++) {
ans1 = Inf, ans2 = -Inf;
KDT::qmin(rt, a[i].v[0], a[i].v[1]);
KDT::qmax(rt, a[i].v[0], a[i].v[1]);
ans = min(ans, ans2 - ans1);
}
cout << ans << '\n';
return 0;
}
例 2 巧克力王国
题目即求所有
但是遗憾的是,此题的
例 3 [国家集训队] JZPFAR
发现题目现在要求离当前点第
由于保证数据随机,所以
例 4 [BZOJ4605] 崂山白花蛇草水
发现这道题就是一个单点加、矩阵第
接下来我们有两种方法来维护:
- 外层维护下标,内层维护权值。即
套权值线段树。 - 外层维护权值,内层维护下标。即权值线段树套
。
第一种做法比较困难,因为这样做的话权值线段树的合并要求可持久化,并且常数也过大。我们采用第二种方法即可,查询时在权值线段树上二分,用
修改的总复杂度是
采用二进制分组的代码如下:
#include <bits/stdc++.h>
#define il inline
using namespace std;
const int Maxn = 1e5 + 5;
const int Inf = 2e9;
const int N = 1e9;
int n, q;
struct node {
int v[2];
}a[Maxn];
int cnt = 0;
namespace KDT {
struct KD_Tree {
int l, r, v[2], mn[2], mx[2], siz;
}t[Maxn * 30];
#define lp t[p].l
#define rp t[p].r
int tot = 0;
int trs[Maxn * 30], top;
int rt[Maxn * 30][18];
il void del(int &p) {
trs[++top] = p;
t[p] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
p = 0;
}
il int newnode() {
return top ? trs[top--] : ++tot;
}
il void pushup(int p) {
t[p].siz = t[lp].siz + t[rp].siz + 1;
t[p].mn[0] = t[p].mx[0] = t[p].v[0];
t[p].mn[1] = t[p].mx[1] = t[p].v[1];
if(lp) {
t[p].mn[0] = min(t[p].mn[0], t[lp].mn[0]);
t[p].mx[0] = max(t[p].mx[0], t[lp].mx[0]);
t[p].mn[1] = min(t[p].mn[1], t[lp].mn[1]);
t[p].mx[1] = max(t[p].mx[1], t[lp].mx[1]);
}
if(rp) {
t[p].mn[0] = min(t[p].mn[0], t[rp].mn[0]);
t[p].mx[0] = max(t[p].mx[0], t[rp].mx[0]);
t[p].mn[1] = min(t[p].mn[1], t[rp].mn[1]);
t[p].mx[1] = max(t[p].mx[1], t[rp].mx[1]);
}
}
void build(int &p, int l, int r, int typ) {
if(l > r) return ;
int mid = (l + r) >> 1;
nth_element(a + l, a + mid, a + r + 1, [typ](node x, node y){return x.v[typ] < y.v[typ];});
p = newnode();
t[p].v[0] = a[mid].v[0], t[p].v[1] = a[mid].v[1];
build(lp, l, mid - 1, typ), build(rp, mid + 1, r, typ);
pushup(p);
}
void append(int &p) {
if(!p) return ;
a[++cnt] = {t[p].v[0], t[p].v[1]};
append(lp), append(rp);
del(p);
}
il bool chkin(int x, int y, int x1, int y1, int x2, int y2) {
return (x1 <= x && x <= x2 && y1 <= y && y <= y2);
}
il bool checkin(int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4) {
return (x3 <= x1 && x2 <= x4 && y3 <= y1 && y2 <= y4);
}
il bool checkout(int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4) {
return (x3 > x2 || x1 > x4 || y3 > y2 || y1 > y4);
}
int query(int p, int x1, int y1, int x2, int y2) {
if(!p) return 0;
if(checkin(t[p].mn[0], t[p].mn[1], t[p].mx[0], t[p].mx[1], x1, y1, x2, y2)) return t[p].siz;//被包含直接返回
if(checkout(t[p].mn[0], t[p].mn[1], t[p].mx[0], t[p].mx[1], x1, y1, x2, y2)) return 0;//没有交点直接返回
int res = 0;
if(chkin(t[p].v[0], t[p].v[1], x1, y1, x2, y2)) {//当前点合法,加入答案
res++;
}
return res + query(lp, x1, y1, x2, y2) + query(rp, x1, y1, x2, y2);//递归求解
}
void ins(int p, node k) {
a[cnt = 1] = k;
for(int i = 0; i < 18; i++) {
if(!rt[p][i]) {
build(rt[p][i], 1, cnt, 0);
break;
}
else append(rt[p][i]);
}
}
int que(int p, int x1, int y1, int x2, int y2) {
int ans = 0;
for(int i = 0; i < 18; i++) {
ans += query(rt[p][i], x1, y1, x2, y2);
}
return ans;
}
}
int rt;
namespace Sgt {
struct Segment_Tree {
int l, r;
}t[Maxn * 30];
int tot = 0;
void mdf(int &p, int l, int r, int x, node k) {
if(!p) p = ++tot;
KDT::ins(p, k);
if(l == r) {
return ;
}
int mid = (l + r) >> 1;
if(x <= mid) mdf(lp, l, mid, x, k);
else mdf(rp, mid + 1, r, x, k);
}
int query(int p, int l, int r, int k, int x1, int y1, int x2, int y2) {
if(!p) return Inf;
if(l == r) {
int ret = KDT::que(p, x1, y1, x2, y2);
return k <= ret ? l : Inf;
}
int mid = (l + r) >> 1;
int res = KDT::que(rp, x1, y1, x2, y2);
if(res < k) return query(lp, l, mid, k - res, x1, y1, x2, y2);
else return query(rp, mid + 1, r, k, x1, y1, x2, y2);
}
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> q;
int lst = 0;
while(q--) {
int typ, x1, y1, x2, y2, k;
cin >> typ;
switch(typ) {
case 1: {
cin >> x1 >> y1 >> k;
x1 ^= lst, y1 ^= lst, k ^= lst;
Sgt::mdf(rt, 1, N, k, (node){x1, y1});
break;
}
case 2: {
cin >> x1 >> y1 >> x2 >> y2 >> k;
x1 ^= lst, y1 ^= lst, x2 ^= lst, y2 ^= lst, k ^= lst;
lst = Sgt::query(rt, 1, N, k, x1, y1, x2, y2);
if(lst == Inf) lst = 0, cout << "NAIVE!ORZzyz.\n";
else cout << lst << '\n';
break;
}
}
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了