k-D Tree 学习笔记

k-D Tree

可以用来处理高维信息。

建树

对于一个 \(k\) 维的空间,记该空间中点数为 \(n\)。执行操作:

  1. 若当前空间中只有一个点,返回。
  2. 若当前空间中不有多个点,选择一个维度 \(d\) 和其中一个点 \(x\)。将 \(x\) 作为当前子树的树根,维度 \(d\) 上比 \(x\) 小的点当作左子树;维度 \(d\) 上比 \(x\) 大的点当作右子树。然后继续对左、右子树建树。

显然,这么建出来的树具有搜索树的性质。

对于 k-D Tree 的建树,一般使用优化:

  1. 维度连续划分,即每连续 \(k\) 层都要能保证这 \(k\) 个维度都被选择过。
  2. 子树大小均衡,一般选择的点都是中位数。这样能够保证树高是 \(\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)\) 为右上角的一个平面。

这样对于空间上的查询,类似线段树区间查询,就分成几种情况:

  1. 查询空间完全包含当前子树空间。直接在当前点上算贡献。
  2. 查询空间不完全包含当前子树空间且有交。算上当前点的贡献后下传到子树。
  3. 查询空间不完全包含当前子树空间且无交。直接返回。

对于时间复杂度,因为每次查询状态减半且维度划分是连续的,那么复杂度为 \(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})\)

posted @ 2026-05-03 15:51  harmis_yz  阅读(20)  评论(0)    收藏  举报