k-D Tree 学习笔记
k-D Tree
可以用来处理高维信息。
建树
对于一个 \(k\) 维的空间,记该空间中点数为 \(n\)。执行操作:
- 若当前空间中只有一个点,返回。
- 若当前空间中不有多个点,选择一个维度 \(d\) 和其中一个点 \(x\)。将 \(x\) 作为当前子树的树根,维度 \(d\) 上比 \(x\) 小的点当作左子树;维度 \(d\) 上比 \(x\) 大的点当作右子树。然后继续对左、右子树建树。
显然,这么建出来的树具有搜索树的性质。
对于 k-D Tree 的建树,一般使用优化:
- 维度连续划分,即每连续 \(k\) 层都要能保证这 \(k\) 个维度都被选择过。
- 子树大小均衡,一般选择的点都是中位数。这样能够保证树高是 \(\log n\) 的。
具体实现
和线段树差不多。因为只需要找到中位数而不需要保证序列有序(只要左边比它小右边比它大),所以可以用 nth_element 优化。时间复杂度 \(O(n\log n)\)。
#define ls(x) (tr[x].ch[0])
#define rs(x) (tr[x].ch[1])
#define nxt(x) ((x + 1) % K)
il void build(int &u, int l, int r, int k){
if(l > r) return u = 0, void(0);
if(l == r) return u = a[l], void(0);
int mid = l + r >> 1;
nth_element(p + l, p + mid, p + r + 1, [k](int x, int y){
return tr[x].x[k] < tr[y].x[k];
});
u = p[mid];
build(ls(u), l, mid - 1, nxt(k));
build(rs(u), mid + 1, r, nxt(k));
return ;
}
查询
k-D Tree 上每个节点维护当前子树对应子空间上每个维度的范围。如 \(l_1=1,r_1=2;l_2=3,r_2=4\) 就表示了以 \((1,3)\) 为左下角,\((2,4)\) 为右上角的一个平面。
这样对于空间上的查询,类似线段树区间查询,就分成几种情况:
- 查询空间完全包含当前子树空间。直接在当前点上算贡献。
- 查询空间不完全包含当前子树空间且有交。算上当前点的贡献后下传到子树。
- 查询空间不完全包含当前子树空间且无交。直接返回。
对于时间复杂度,因为每次查询状态减半且维度划分是连续的,那么复杂度为 \(T(n) = 2^{k-1}T(\frac{n}{2^{k}}) + O(1)\)。也就是 \(O(n^{\frac{k-1}{k}})\)。
以查询平面点权值和为例,就可以这么写:
#define ls(x) (tr[x].ch[0])
#define rs(x) (tr[x].ch[1])
#define nxt(x) ((x + 1) % K)
const int N = 1e5 + 10, K = 2;
struct node{
int ch[2];
int x[K];
int sum, val;
int l[K], r[K];
} tr[N];
int p[N];
struct Qry{
int ql[K], qr[K];
int ans;
} q;
il void up(int u){
UU(k, 0, k0){
tr[u].l[k] = min({tr[ls(u)].l[k], tr[u].x[k], tr[rs(u)].l[k]});
tr[u].r[k] = max({tr[ls(u)].r[k], tr[u].x[k], tr[rs(u)].r[k]});
}
tr[u].sum = tr[ls(u)].sum + tr[u].val + tr[rs(u)].sum;
return ;
}
il void build(int &u, int l, int r, int k){
if(l > r) return ;
int mid = l + r >> 1;
nth_element(p + l, p + mid, p + r + 1, [k](int x, int y){
return tr[x].x[k] < tr[y].x[k];
});
u = p[mid];
tr[u].sum = tr[u].val;
UU(x, 0, K){
tr[u].l[x] = tr[u].x[x];
tr[u].r[x] = tr[u].x[x];
}
build(ls(u), l, mid - 1, nxt(k));
build(rs(u), mid + 1, r, nxt(k));
up(u);
return ;
}
il int qry(int u){
UU(k, 0, K) if(tr[u].r[k] < q.ql[k] || q.qr[k] < tr[u].l[k]) return 0;
bool flg = 1;
UU(k, 0, K) flg &= (q.ql[k] <= tr[u].l[k] && tr[u].r[k] <= q.qr[k]);
if(flg) return tr[u].sum;
flg = 1;
UU(k, 0, K) flg &= (q.ql[k] <= tr[u].x[k] && tr[u].x[k] <= q.qr[k]);
return qry(ls(u)) + qry(rs(u)) + tr[u].val * flg;
}
插入/删除
二进制分组
维护若干棵大小为 \(2^x\) 的树,保证树的大小互不相同。那么每次插入一个点,就相当于得到一个大小为 \(2^0\) 的树。然后不断合并大小相同的树。
这样每个点最多贡献 \(O(\log n)\) 次合并,所以复杂度均摊 \(O(n\log^2 n)\)。而对于查询,根据等比数列求和也是 \(O(n^{\frac{k-1}{k}})\) 的。
根号重构
这个比上面那个不牛,但是在删除的时候可以节省空间(?),因为二进制分组删点只能在树上清空贡献而不能真的删掉。
略。
例题
P14312 【模板】K-D Tree
P4148 是弱化版。
带修带插入多维空间求和。
直接做就行了,复杂度 \(O(m^{\frac{k+1}{k}})\)。
给个例子:
#define ls(x) (tr[x].ch[0])
#define rs(x) (tr[x].ch[1])
#define nxt(x) ((x + 1) % k0)
const int N = 2e5 + 10, K = 3, inf = 1e18 + 7;
struct node{
int ch[2];
int x[K];
int sum, val, cnt;
int l[K], r[K];
int tag;
} tr[N];
int p[N], len;
struct Qry{
int ql[K], qr[K];
int val;
} q, c;
int root[20], idx;
int k0, m;
il void up(int u){
UU(k, 0, k0){
tr[u].l[k] = min({tr[ls(u)].l[k], tr[u].x[k], tr[rs(u)].l[k]});
tr[u].r[k] = max({tr[ls(u)].r[k], tr[u].x[k], tr[rs(u)].r[k]});
}
tr[u].cnt = tr[ls(u)].cnt + 1 + tr[rs(u)].cnt;
tr[u].sum = tr[ls(u)].sum + tr[u].val + tr[rs(u)].sum;
return ;
}
il void down(int u){
if(ls(u)){
tr[ls(u)].sum += tr[ls(u)].cnt * tr[u].tag;
tr[ls(u)].val += tr[u].tag;
tr[ls(u)].tag += tr[u].tag;
}
if(rs(u)){
tr[rs(u)].sum += tr[rs(u)].cnt * tr[u].tag;
tr[rs(u)].val += tr[u].tag;
tr[rs(u)].tag += tr[u].tag;
}
tr[u].tag = 0;
return ;
}
il void build(int &u, int l, int r, int k){
u = 0;
if(l > r) return ;
int mid = l + r >> 1;
nth_element(p + l, p + mid, p + r + 1, [k](int x, int y){
return tr[x].x[k] < tr[y].x[k];
});
u = p[mid];
tr[u].cnt = 1;
tr[u].tag = 0;
tr[u].sum = tr[u].val;
UU(x, 0, k0){
tr[u].l[x] = tr[u].x[x];
tr[u].r[x] = tr[u].x[x];
}
down(u);
build(ls(u), l, mid - 1, nxt(k));
build(rs(u), mid + 1, r, nxt(k));
up(u);
return ;
}
il void mdf(int u){
if(!u) return ;
UU(k, 0, k0) if(tr[u].r[k] < c.ql[k] || c.qr[k] < tr[u].l[k]) return ;
bool flg = 1;
UU(k, 0, k0) flg &= (c.ql[k] <= tr[u].l[k] && tr[u].r[k] <= c.qr[k]);
if(flg){
tr[u].val += c.val;
tr[u].sum += tr[u].cnt * c.val;
tr[u].tag += c.val;
return ;
}
flg = 1;
UU(k, 0, k0) flg &= (c.ql[k] <= tr[u].x[k] && tr[u].x[k] <= c.qr[k]);
down(u);
tr[u].val += c.val * flg;
mdf(ls(u));
mdf(rs(u));
up(u);
return ;
}
il int qry(int u){
if(!u) return 0;
UU(k, 0, k0) if(tr[u].r[k] < q.ql[k] || q.qr[k] < tr[u].l[k]) return 0;
bool flg = 1;
UU(k, 0, k0) flg &= (q.ql[k] <= tr[u].l[k] && tr[u].r[k] <= q.qr[k]);
if(flg) return tr[u].sum;
flg = 1;
UU(k, 0, k0) flg &= (q.ql[k] <= tr[u].x[k] && tr[u].x[k] <= q.qr[k]);
down(u);
return qry(ls(u)) + qry(rs(u)) + tr[u].val * flg;
}
il void G(int u){
if(!u) return ;
down(u);
p[++ len] = u;
G(ls(u)), G(rs(u));
return ;
}
il void solve(){
k0 = rd, m = rd;
int lst = 0;
UU(i, 0, k0) tr[0].l[i] = inf, tr[0].r[i] = -inf;
while(m --){
int op = rd;
if(op == 1){
len = 0;
p[++ len] = ++idx;
UU(i, 0, k0) tr[idx].x[i] = (rd ^ lst);
tr[idx].val = (rd ^ lst);
int u = 0;
while(root[u]){
G(root[u]);
root[u] = 0;
++ u;
}
build(root[u], 1, len, 0);
}
else if(op == 2){
UU(i, 0, k0) c.ql[i] = (rd ^ lst);
UU(i, 0, k0) c.qr[i] = (rd ^ lst);
c.val = (rd ^ lst);
U(i, 0, 19) if(root[i]) mdf(root[i]);
}
else{
UU(i, 0, k0) q.ql[i] = (rd ^ lst);
UU(i, 0, k0) q.qr[i] = (rd ^ lst);
int res = 0;
U(i, 0, 19) if(root[i]) res += qry(root[i]);
cout << res << "\n";
lst = res;巧克力王国
}
}
return ;
}
P3769 [CH弱省胡策R2] TATT
DP 状态是简单的,考虑优化。
按照 \(a\) 从小到达排序后就是一个 \(3\) 维空间求最大值的问题了。直接做时间复杂度 \(O(n^{\frac{5}{3}})\)。
而使用树状树组维护 \(b\) 这一维,对剩下 \(2\) 维做 k-D Tree 的时间复杂度就是 \(O(n^{\frac{3}{2}})\) 了,因为 \(\sum\limits_{i=1}^{\log n}\sqrt{2^i}\le 2\sum\limits_{i=1}^{\log\sqrt{n}}2^i = O(\sqrt{n})\)。
P14676 [ICPC 2025 Seoul R] Mex Culpa
首先让 \(b_i \gets b_i + a_i\)。
枚举 \(\operatorname{mex}=x\),将 \(f_i \le x-1\) 的点删掉后,显然如果 \(f_i =x\) 应该满足:
- 不存在 \(j\),使得 \(1 \le j <i \land a_j \le b_i \land a_i \le b_j\)。
那么从小到大枚举 \(x\),每次将 \(f_{i}=x\) 的点找到然后删掉,对一个 \(3\) 维空间中的点减去 \(1\)。那么每个时刻权值为 \(0\) 的点一定是满足条件的点。维护最小值然后暴力删就行了。这样每个点最多被删 \(1\) 次,且被删的时候贡献 \(O(\log n)\) 的时间复杂度。总共是 \(O(n\log n)\) 的。但是 k-D Tree 部分是 \(O(n^{\frac{5}{3}})\) 的。
将 \([a_j,b_j]\) 看成一个区间。上述条件等价于 \([1,i-1]\) 中有一个点对应的区间和 \([a_i,b_i]\) 有交。则 \(f\) 值相同的点对应的区间一定两两无交。
这样首先找到全局最小编号的点 \(p\) 和它对应的区间 \([l,r]\)。因为剩下的 \(f=x\) 的点 \(q\) 一定满足 \([a_q,b_q]\subseteq [1,l] \cup [r,+\infin)\),所以就变成两个区间查询编号最小点的问题了。显然这是子问题。
那么一个点只会贡献 \(O(1)\) 次查询 \(2\) 维平面权值最小值点,总时间复杂度 \(O(n\log^2 n)\) 或者 k-D Tree 的 \(O(n\sqrt{n})\)。

浙公网安备 33010602011771号