kd-tree

k dimensional tree

用途

用于多维空间的搜索查询
(比如地图地理空间, 甚至搜索引擎多属性)
本质上是一颗二叉树, 里面的每个节点代表了一个矩形区域的切割

复杂度

首先平衡和不平衡的kdtree复杂度不一样, 不平衡的kdtree指的是所有的点都在同一侧, 以至于复杂度退化到了O(n), 面对不平衡的kdtree我们需要重构, 这个之后会说

  1. 插入
    在平衡的情况下是O(logn)
  2. 构建
    离线查询, 一开始给全数据, 那自然就是O(n logn), 且很平衡
    但是若是在线查询, 数据不一口气给出的话, 加上每次拍平重建(我选择替罪羊树维护), 则是取决于数据是否严重不平衡(自选平衡因子). 这里我不知道最坏时间复杂度是多少, 我盲猜一个O(n^(3/2) logn)
  3. 搜索
    平衡的情况下是O(logn)
  4. 删除
    删除一般选择lazy标记, 重新构建的时候才会真正删除
    平衡情况下依然是O(nlogn)

可以知道在不平衡的情况下, 一次查询或删除时间复杂度是O(n)

数据结构的选择

  1. 点的数据结构
    首先有几个维度就应该有个几维的数组
    我们设维度为K
    int x[K]
    然后既然存树, 那也许会有个权重, 不排除有的题目没有, 那样的话就是简单求求点与点之间的距离, 不需要维护就存在的一些属性
struct Point{
int x[2];
int w;
}pointspool[MAXSIZE];
int pointidx;
  1. 树的数据结构
    可能会问了, 树不就是点么
    是的, 树可以是点, 但是也可以分开存, 方便理解, 切割就干切割的事情, 存储就干存储的事情, 无所谓的, 因为很多情况下, 原始数据可以不那么重要, 在树的点上维护新的数据是需要的, 仁者见仁智者见智吧, 而且insert里都要传参, 那其实直接指向还可以节省一次copy
    如果是需要重建平衡的情况, 我们还需要增加一些其他属性, left和right指向切割的矩阵, 维护size确定平衡度, 还可以自己维护一些需要的属性等等
struct TreeNode
{
int _max[2], _min[2];
Point *p;
TreeNode *left, *right;
int size;
};
TreeNode ptree[MAXSIZE]; // 动态节点pool
TreeNode *recycle[MAXSIZE]; // 删除回收的时候当个中介
int poolidx; // 标记一下ptree分配的位置
  1. 需要一个root节点
TreeNode *root;
  1. 需要一个全局k
    nth_element的时候需要用来比较大小, 我这里变量名叫dim
nth_element(parr + l, parr + mid, parr + r + 1, [](const Point *a, const Point *b)
{ return a->x[dim] < b->x[dim]; });
  1. 需要一个中间数组池存储重建树
Point *parr[MAXSIZE];

动态开点

这里还要照顾一下未来删点或者重建时候, 要回收空间使用

TreeNode ptree[MAXSIZE];
TreeNode *recycle[MAXSIZE];
int top, poolidx;
TreeNode *newnode()
{
if (top)
{
return recycle[top--];
}
else
{
return &ptree[++poolidx];
}
}

insert

insert很简单
root不存在就建root
root存在就跟二叉树一样往下按当前dimension的值走, 递归建树
直到为空
最后统一走一次up()和check()
up是为了维护题目中要求的数据, 或者搞点边界条件将来查询的时候剪枝用, 我一般维护一下max和min的值将来剪枝用
check是为了平衡树高, check我偶尔也喜欢放到整棵树插完之后直接check root

void insert(TreeNode *&cur, Point &p, int dimension)
{
if (cur == nullptr)
{
cur = newnode();
cur->left = cur->right = nullptr;
cur->p = &p;
cur->size = 0;
up(cur);
return;
}
if (p.x[dimension] <= cur->p->x[dimension])
{
insert(cur->left, p, dimension ^ 1);
}
else
{
insert(cur->right, p, dimension ^ 1);
}
up(cur);
check(cur, 0);
}

讲下up和check

up其实就是向上更新, size维护一下, 需要的边界维护一下

void up(TreeNode *cur)
{
auto child = {cur->left, cur->right};
cur->size = 1;
for (int i = 0; i < k; i++)
{
cur->_max[i] = cur->_min[i] = cur->p->x[i];
for (auto ch : child)
{
if (ch)
{
cur->_max[i] = std::max(cur->_max[i], ch->_max[i]);
cur->_min[i] = std::min(cur->_min[i], ch->_min[i]);
if (i == 0)
cur->size += ch->size;
}
}
}
}

check就是检测一下平衡, 这里我其实没有明确懂得每个节点递归检测check的意义
在我看来, 统一root检测一下, 连dimension都不需要传了
靠, 我有时候真的怀疑维护平衡真的有意义吗? 为什么很多时候维护平衡反而是副作用, 重建一次复杂度也不低, 最少都是O(常数*n)的时间

void check(TreeNode *&cur, int dimension)
{
if ((cur->left && BALANCE * cur->size < cur->left->size) ||
(cur->right && BALANCE * cur->size < cur->right->size))
{
ldr(cur, 0); // 中序遍历二叉树, 输出->数组, 有时候也在思考这个数组中序输出到底有没有意义, 我感觉意义不大
cur = build(1, cur->size, dimension); // 重新build, 在后文
}
}

删除

这里删除建议直接把那个点给惰性标记
选需要删除的点很多的时候, 再真正删除(可以记个数, 也可以搞个比例因子)
当重建树的时候, 也可以顺便删掉.
删除很少的话, 也可以不删

建树

在不需要重构的建树里, 我们可以选择和线段树一样的, idx<<1和idx<<1|1表示idx节点的左右儿子
而在需要重构的建树里, 我们需要动态开点, 动态开点有很多种写法, 为了直观一些, 我就写指针的写法
递归建树即可, 跟其他动态开点树一样的写法

TreeNode *build(int l, int r, int dimension)
{
if (l > r)
{
return nullptr;
}
int mid = (l + r) >> 1;
TreeNode *cur = newnode();
dim = dimension;
nth_element(parr + l, parr + mid, parr + r + 1, [](const Point *a, const Point *b)
{ return a->x[dim] < b->x[dim]; });
cur->p = parr[mid];
cur->left = build(l, mid - 1, dimension ^ 1);
cur->right = build(mid + 1, r, dimension ^ 1);
up(cur);
return cur;
}

重建

遍历二叉排序树, 放到一个数组里, 然后重新建一遍就好了

query

感觉query才是重点
然而也没什么好讲的, 有点启发式的味道

void query(TreeNode *cur, int x[2], int depth = 0)
{
int curdim = depth % 2;
TreeNode *l = cur->left;
TreeNode *r = cur->right;
if (l && x[curdim] >= l->p->x[curdim])
{
std::swap(l, r);
}
if (l && l->size && cal_intersect(x, l))
{
query(l, x, depth + 1);
}
// 在这里处理这个点
if (r && r->size && cal_intersect(x, r))
{
query(r, x, depth + 1);
}
}

先遍历更可能成为答案的
然后用之前存的条件把枝给剪了, 不要去不可能的分支上
这里一般涉及几何的一些剪枝
举个曼哈顿剪枝的例子, 矩形到圆的剪枝, 欧式距离改成平方即可

bool cal_intersect(int x[2], TreeNode *cur)
{
int dx = std::max(cur->_min[0] - x[0], std::max(0, x[0] - cur->_max[0]));
int dy = std::max(cur->_min[1] - x[1], std::max(0, x[1] - cur->_max[1]));
return dx + dy <= L;
}

相关题目

离线: HDU4347
在线: P4148

posted on   tianlonghuangwu  阅读(32)  评论(0编辑  收藏  举报

相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现

导航

< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5
点击右上角即可分享
微信分享提示