kd-tree
k dimensional tree
用途
用于多维空间的搜索查询
(比如地图地理空间, 甚至搜索引擎多属性)
本质上是一颗二叉树, 里面的每个节点代表了一个矩形区域的切割
复杂度
首先平衡和不平衡的kdtree复杂度不一样, 不平衡的kdtree指的是所有的点都在同一侧, 以至于复杂度退化到了O(n), 面对不平衡的kdtree我们需要重构, 这个之后会说
- 插入
在平衡的情况下是O(logn) - 构建
离线查询, 一开始给全数据, 那自然就是O(n logn), 且很平衡
但是若是在线查询, 数据不一口气给出的话, 加上每次拍平重建(我选择替罪羊树维护), 则是取决于数据是否严重不平衡(自选平衡因子). 这里我不知道最坏时间复杂度是多少, 我盲猜一个O(n^(3/2) logn) - 搜索
平衡的情况下是O(logn) - 删除
删除一般选择lazy标记, 重新构建的时候才会真正删除
平衡情况下依然是O(nlogn)
可以知道在不平衡的情况下, 一次查询或删除时间复杂度是O(n)
数据结构的选择
- 点的数据结构
首先有几个维度就应该有个几维的数组
我们设维度为K
int x[K]
然后既然存树, 那也许会有个权重, 不排除有的题目没有, 那样的话就是简单求求点与点之间的距离, 不需要维护就存在的一些属性
struct Point{ int x[2]; int w; }pointspool[MAXSIZE]; int pointidx;
- 树的数据结构
可能会问了, 树不就是点么
是的, 树可以是点, 但是也可以分开存, 方便理解, 切割就干切割的事情, 存储就干存储的事情, 无所谓的, 因为很多情况下, 原始数据可以不那么重要, 在树的点上维护新的数据是需要的, 仁者见仁智者见智吧, 而且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分配的位置
- 需要一个root节点
TreeNode *root;
- 需要一个全局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]; });
- 需要一个中间数组池存储重建树
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 2023-11-22 20:41 tianlonghuangwu 阅读(32) 评论(0) 编辑 收藏 举报
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现