k-D Tree (KDT, k-Dimension Tree) is a data structure that can efficiently process -dimensional spatial information.
When the number of nodes is much greater than , a k-D Tree is highly efficient.
In programming-contest problems, is typical. When analyzing time complexity on this page, we treat as a constant.
Building the tree
A k-D Tree has the shape of a binary search tree. Each node in the binary search tree corresponds to a point in -dimensional space. The points in each subtree lie within a -dimensional hyperrectangle, and every point in that hyperrectangle belongs to the subtree.
Suppose we know the coordinates of points in -dimensional space, with distinct points in total. To build a k-D Tree from them, proceed as follows:
-
If the current hyperrectangle contains only one point, return that point.
-
Choose a dimension and divide the current hyperrectangle into two hyperrectangles along that dimension.
-
Choose a split point: select a point along the chosen dimension; points whose value in that dimension is smaller than it go into one hyperrectangle (the left subtree), and the rest go into the other (the right subtree).
-
Use the selected point as the root of this subtree, recursively build the left and right subtrees from the two hyperrectangles, and maintain subtree information.
For intuition, consider an example with .

The k-D Tree constructed from it may have the following shape:

Each node's coordinates are those of the selected split point; the or beside a non-leaf node indicates the selected splitting dimension.
This complexity is not guaranteed. For steps , we introduce two optimizations:
- Cycle through the dimensions so that every dimension is split within any consecutive levels.
- Each time a split point is chosen along a dimension, choose the median on that dimension, so that the two resulting subtrees are as nearly equal in size as possible.
With optimization , the height of the constructed k-D Tree is at most .
The bottleneck in constructing a k-D Tree is quickly selecting the median along a dimension, placing values smaller than the median to its left and the rest to its right. Sorting by that dimension with the sort function each time takes . In fact, finding the median of elements and placing it in its correct sorted position can be done in .
We recall the idea of quicksort. Each time, choose a number, put smaller values to its left and larger values to its right so that it occupies its correct sorted position, then recursively sort both sides. The expected complexity is . However, because a k-D Tree only requires the median to be in its correct sorted position, we need recurse into only one side containing the median. This can be shown to have expected complexity . The algorithm library provides a function nth_element() with the same behavior: to find the value between s[l] and s[r] that, after sorting according to cmp, would occupy position s[mid], while ensuring values to the left of s[mid] are smaller than s[mid] and values to the right are larger than s[mid], simply write nth_element(s+l,s+mid,s+r+1,cmp).
With this idea, the time complexity of constructing a k-D Tree is .
Operations in high-dimensional space
When querying information about all points inside a high-dimensional rectangular region, record the maximum and minimum coordinate in each dimension among the points in each node's subtree. If the rectangle corresponding to the current subtree does not intersect the query rectangle, do not search that subtree further; if it is completely contained in the query rectangle, return the sum of the weights of all points in the subtree; otherwise, check whether the current point lies in the query rectangle, update the answer, and recursively search both subtrees.
Implementation
int query(int p) {
if (!p) return 0;
bool flag{false};
for (int k : {0, 1}) flag |= (!(l.x[k] <= t[p].L[k] && t[p].R[k] <= h.x[k]));
if (!flag) return t[p].sum;
for (int k : {0, 1})
if (t[p].R[k] < l.x[k] || h.x[k] < t[p].L[k]) return 0;
int ans{0};
flag = false;
for (int k : {0, 1}) flag |= (!(l.x[k] <= t[p].x[k] && t[p].x[k] <= h.x[k]));
if (!flag) ans = t[p].v;
return ans += query(t[p].l) + query(t[p].r);
}
Complexity analysis
First consider the two-dimensional case. When querying a rectangle , classify the nodes of the k-D Tree into three types:
- Disjoint from .
- Completely contained in .
- Partially contained in .
Clearly, the complexity of one query is the number of type-3 points. A type-3 point's rectangle either completely contains or is disjoint from it; the former contributes only points. We now analyze the number of the latter.
First, we may shift every side of the rectangle by so that the query rectangle passes through none of the existing points. This clearly does not change the set of points covered by the query rectangle.
Observe that each rectangle corresponding to a disjoint type-3 point must be crossed by one side of . Therefore, we only need to count how many rectangles each side of crosses, that is, the maximum number of point-associated rectangles crossed by any line segment.
Consider a node with four grandchildren, where reaching each grandchild involves splitting once in each of the two dimensions. Observe that splitting a rectangle into four subrectangles this way lets a line segment parallel to a coordinate axis pass through at most two regions. Thus, starting from , a query can descend into at most two grandchildren that still contain type-3 points (if the segment lies exactly on a split boundary this need not hold, but shifting the query rectangle's boundary avoids that case).
When building the tree, each point is the median of its entire subtree in the current splitting dimension, so the subtree size is halved. Thus, if the subtree of has size , we obtain the following recurrence:
By the Master Theorem, .
Generalizing the recurrence to dimensions gives , and hence (treating as a constant).
Insertions/deletions
If this -dimensional point set is dynamic, meaning that points may be inserted or deleted, the balance of the k-D Tree cannot be guaranteed. Because of how a k-D Tree is constructed, rotations are not supported, and random priorities like those in an FHQ Treap cannot guarantee the complexity either. There are two common maintenance methods.
Note
Many contestants use a scapegoat tree for maintenance. However, the preceding complexity analysis requires each child's subtree to be strictly halved, so the height must be strictly , whereas a scapegoat tree only guarantees height ; the query complexity therefore cannot be guaranteed.
Square-root rebuilding
On insertion, first store the point to be inserted and rebuild after every insertions.
For deletion, simply mark the point. If stricter guarantees are needed, maintain the number of deleted points in the tree and rebuild when it reaches .
The amortized update complexity is and the query complexity is . If the two operation counts are of the same order, the optimum is (update , query ).
Binary grouping
Consider maintaining several k-D Trees whose sizes are powers of , with the sum of their sizes equal to .
On insertion, create a k-D Tree of size , then repeatedly merge trees of equal size (flattening and rebuilding them directly). In an implementation, it is enough to rebuild only once.
The trees that need to be merged always have sizes starting at with consecutive exponents. The complexity is similar to binary addition: amortized , because rebuilding itself costs a factor of .
For a query, query each tree separately. The complexity is .
Example problems
Luogu P4148 Simple Problem
Initially filled with , an two-dimensional matrix undergoes operations, each of one of the following two types:
1 x y A: At , add .2 x1 y1 x2 y2: Output the sum of the values in the rectangle with as its lower-left corner and as its upper-right corner, including the boundary.
Online processing is mandatory. The memory limit is 20M. The answer and all intermediate values fit in int.
The 20M memory limit rules out all segment-tree-over-tree structures, and mandatory online processing rules out CDQ divide and conquer; only a k-D Tree can be used.
The following is reference code for binary grouping.
Reference code
#include <algorithm>
#include <iostream>
using namespace std;
constexpr int N(2e5), LG{18};
struct pt {
int x[2];
int v, sum;
int l, r;
int L[2], R[2];
} t[N + 5], l, h;
int rt[LG];
int b[N + 5], cnt;
void upd(int p) {
t[p].sum = t[t[p].l].sum + t[t[p].r].sum + t[p].v;
for (int k : {0, 1}) {
t[p].L[k] = t[p].R[k] = t[p].x[k];
if (t[p].l) {
t[p].L[k] = min(t[p].L[k], t[t[p].l].L[k]);
t[p].R[k] = max(t[p].R[k], t[t[p].l].R[k]);
}
if (t[p].r) {
t[p].L[k] = min(t[p].L[k], t[t[p].r].L[k]);
t[p].R[k] = max(t[p].R[k], t[t[p].r].R[k]);
}
}
}
int build(int l, int r, int dep = 0) {
int p{(l + r) >> 1};
nth_element(b + l, b + p, b + r + 1,
[dep](int x, int y) { return t[x].x[dep] < t[y].x[dep]; });
int x{b[p]};
if (l < p) t[x].l = build(l, p - 1, dep ^ 1);
if (p < r) t[x].r = build(p + 1, r, dep ^ 1);
upd(x);
return x;
}
void append(int &p) {
if (!p) return;
b[++cnt] = p;
append(t[p].l);
append(t[p].r);
p = 0;
}
int query(int p) {
if (!p) return 0;
bool flag{false};
for (int k : {0, 1}) flag |= (!(l.x[k] <= t[p].L[k] && t[p].R[k] <= h.x[k]));
if (!flag) return t[p].sum;
for (int k : {0, 1})
if (t[p].R[k] < l.x[k] || h.x[k] < t[p].L[k]) return 0;
int ans{0};
flag = false;
for (int k : {0, 1}) flag |= (!(l.x[k] <= t[p].x[k] && t[p].x[k] <= h.x[k]));
if (!flag) ans = t[p].v;
return ans += query(t[p].l) + query(t[p].r);
}
int main() {
int n;
cin >> n;
int lst{0};
n = 0;
while (true) {
int op;
cin >> op;
if (op == 1) {
int x, y, A;
cin >> x >> y >> A;
x ^= lst;
y ^= lst;
A ^= lst;
t[++n] = {{x, y}, A};
b[cnt = 1] = n;
for (int sz{0};; ++sz)
if (!rt[sz]) {
rt[sz] = build(1, cnt);
break;
} else
append(rt[sz]);
} else if (op == 2) {
cin >> l.x[0] >> l.x[1] >> h.x[0] >> h.x[1];
l.x[0] ^= lst;
l.x[1] ^= lst;
h.x[0] ^= lst;
h.x[1] ^= lst;
lst = 0;
for (int i{0}; i < LG; ++i) lst += query(rt[i]);
cout << lst << "\n";
} else
break;
}
return 0;
}
Neighborhood queries
Warning
The worst-case time complexity of a single nearest-point query with a k-D Tree is still , but it remains a useful heuristic algorithm for scoring points; use it with care. Here, the discussion of neighborhood queries is limited to deepening understanding of the k-D Tree structure.
Example: Luogu P1429 — Closest Pair of Points (Enhanced)
Given points in the plane, find the Euclidean distance between the closest pair of points in the plane.
First build a 2-D Tree for these points.
Enumerate each node and, for each node, find the closest point other than that node to obtain the answer. A brute-force traversal of every node in a 2-D Tree takes time per query, so pruning is needed. We can maintain the minimum and maximum coordinate in each dimension among all points in a subtree. Suppose the closest-pair distance found so far is ; if the minimum distance from the query point to the rectangle containing all points in the subtree is at least , then the subtree cannot contain the answer and is skipped.
Moreover, we can use a heuristic search: if both subtrees of a node may contain the answer, search first in the subtree closer to the query point. The minimum distance from the query point to the rectangle corresponding to a subtree is the evaluation function for this problem.
Reference code
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <iomanip>
#include <iostream>
using namespace std;
constexpr int MAXN = 200010;
int n, d[MAXN], lc[MAXN], rc[MAXN];
double ans = 2e18;
struct node {
double x, y;
} s[MAXN];
double L[MAXN], R[MAXN], D[MAXN], U[MAXN];
double dist(int a, int b) {
return (s[a].x - s[b].x) * (s[a].x - s[b].x) +
(s[a].y - s[b].y) * (s[a].y - s[b].y);
}
bool cmp1(node a, node b) { return a.x < b.x; }
bool cmp2(node a, node b) { return a.y < b.y; }
void maintain(int x) {
L[x] = R[x] = s[x].x;
D[x] = U[x] = s[x].y;
if (lc[x])
L[x] = min(L[x], L[lc[x]]), R[x] = max(R[x], R[lc[x]]),
D[x] = min(D[x], D[lc[x]]), U[x] = max(U[x], U[lc[x]]);
if (rc[x])
L[x] = min(L[x], L[rc[x]]), R[x] = max(R[x], R[rc[x]]),
D[x] = min(D[x], D[rc[x]]), U[x] = max(U[x], U[rc[x]]);
}
int build(int l, int r) {
if (l > r) return 0;
if (l == r) {
maintain(l);
return l;
}
int mid = (l + r) >> 1;
double avx = 0, avy = 0, vax = 0, vay = 0; // average variance
for (int i = l; i <= r; i++) avx += s[i].x, avy += s[i].y;
avx /= (double)(r - l + 1);
avy /= (double)(r - l + 1);
for (int i = l; i <= r; i++)
vax += (s[i].x - avx) * (s[i].x - avx),
vay += (s[i].y - avy) * (s[i].y - avy);
if (vax >= vay)
d[mid] = 1, nth_element(s + l, s + mid, s + r + 1, cmp1);
else
d[mid] = 2, nth_element(s + l, s + mid, s + r + 1, cmp2);
lc[mid] = build(l, mid - 1), rc[mid] = build(mid + 1, r);
maintain(mid);
return mid;
}
double f(int a, int b) {
double ret = 0;
if (L[b] > s[a].x) ret += (L[b] - s[a].x) * (L[b] - s[a].x);
if (R[b] < s[a].x) ret += (s[a].x - R[b]) * (s[a].x - R[b]);
if (D[b] > s[a].y) ret += (D[b] - s[a].y) * (D[b] - s[a].y);
if (U[b] < s[a].y) ret += (s[a].y - U[b]) * (s[a].y - U[b]);
return ret;
}
void query(int l, int r, int x) {
if (l > r) return;
int mid = (l + r) >> 1;
if (mid != x) ans = min(ans, dist(x, mid));
if (l == r) return;
double distl = f(x, lc[mid]), distr = f(x, rc[mid]);
if (distl < ans && distr < ans) {
if (distl < distr) {
query(l, mid - 1, x);
if (distr < ans) query(mid + 1, r, x);
} else {
query(mid + 1, r, x);
if (distl < ans) query(l, mid - 1, x);
}
} else {
if (distl < ans) query(l, mid - 1, x);
if (distr < ans) query(mid + 1, r, x);
}
}
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
cin >> n;
for (int i = 1; i <= n; i++) cin >> s[i].x >> s[i].y;
build(1, n);
for (int i = 1; i <= n; i++) query(1, n, i);
cout << fixed << setprecision(4) << sqrt(ans) << '\n';
return 0;
}
Example: “CQOI2016” K-th Farthest Pair
Given points in the plane, find the distance between the -th farthest unordered pair under Euclidean distance.
Similar to the previous problem, we replace the closest pair with the -th farthest pair and change the evaluation function to the maximum distance from the query point to the rectangle corresponding to a subtree. Maintain the distances of the current farthest pairs in a min-heap; if a newly found pair has distance greater than the heap top, pop the top and insert the new distance. Use the heap top for pruning as well.
Because the problem asks for unordered pairs, swapping the two endpoints still gives the same pair, so every ordered pair is counted twice; therefore, multiply the input by .
Reference code
#include <algorithm>
#include <cstring>
#include <iostream>
#include <queue>
using namespace std;
constexpr int MAXN = 100010;
long long n, k;
priority_queue<long long, vector<long long>, greater<long long>> q;
struct node {
long long x, y;
} s[MAXN];
bool cmp1(node a, node b) { return a.x < b.x; }
bool cmp2(node a, node b) { return a.y < b.y; }
long long lc[MAXN], rc[MAXN], L[MAXN], R[MAXN], D[MAXN], U[MAXN];
void maintain(int x) {
L[x] = R[x] = s[x].x;
D[x] = U[x] = s[x].y;
if (lc[x])
L[x] = min(L[x], L[lc[x]]), R[x] = max(R[x], R[lc[x]]),
D[x] = min(D[x], D[lc[x]]), U[x] = max(U[x], U[lc[x]]);
if (rc[x])
L[x] = min(L[x], L[rc[x]]), R[x] = max(R[x], R[rc[x]]),
D[x] = min(D[x], D[rc[x]]), U[x] = max(U[x], U[rc[x]]);
}
int build(int l, int r) {
if (l > r) return 0;
int mid = (l + r) >> 1;
double av1 = 0, av2 = 0, va1 = 0, va2 = 0; // average variance
for (int i = l; i <= r; i++) av1 += s[i].x, av2 += s[i].y;
av1 /= (r - l + 1);
av2 /= (r - l + 1);
for (int i = l; i <= r; i++)
va1 += (av1 - s[i].x) * (av1 - s[i].x),
va2 += (av2 - s[i].y) * (av2 - s[i].y);
if (va1 > va2)
nth_element(s + l, s + mid, s + r + 1, cmp1);
else
nth_element(s + l, s + mid, s + r + 1, cmp2);
lc[mid] = build(l, mid - 1);
rc[mid] = build(mid + 1, r);
maintain(mid);
return mid;
}
long long sq(long long x) { return x * x; }
long long dist(int a, int b) {
return max(sq(s[a].x - L[b]), sq(s[a].x - R[b])) +
max(sq(s[a].y - D[b]), sq(s[a].y - U[b]));
}
void query(int l, int r, int x) {
if (l > r) return;
int mid = (l + r) >> 1;
long long t = sq(s[mid].x - s[x].x) + sq(s[mid].y - s[x].y);
if (t > q.top()) q.pop(), q.push(t);
long long distl = dist(x, lc[mid]), distr = dist(x, rc[mid]);
if (distl > q.top() && distr > q.top()) {
if (distl > distr) {
query(l, mid - 1, x);
if (distr > q.top()) query(mid + 1, r, x);
} else {
query(mid + 1, r, x);
if (distl > q.top()) query(l, mid - 1, x);
}
} else {
if (distl > q.top()) query(l, mid - 1, x);
if (distr > q.top()) query(mid + 1, r, x);
}
}
int main() {
cin >> n >> k;
k *= 2;
for (int i = 1; i <= k; i++) q.push(0);
for (int i = 1; i <= n; i++) cin >> s[i].x >> s[i].y;
build(1, n);
for (int i = 1; i <= n; i++) query(1, n, i);
cout << q.top() << endl;
return 0;
}
Exercises
“Violet” Angel Doll/SJY Place Chess Pieces