K-D Tree

K-D Tree可以搞多维空间问题,其形式为一棵二叉搜索树,它能把一张高维(>=2)的图分成好多块,其节点的某维坐标大于其左儿子,小于其右儿子。

K-D Tree的建立

对于k维的问题,第i层我们根据区间内各点的第(i % k)维坐标,用快速排序的思想,可以做到 log 时间内找中位数,然后以中位数的节点为当前根,把当前区间一分为二,然后(如果有的话)递归到其左右区间,左右区间的根即为当前根的左右儿子。如图:

k-d tree形态

【卡常技巧】最好把 \(ls,rs,val,siz\) 之类的一块放在结构体里面,据说会因为“连续”而变快。

\(Code:\)

//二维k-d tree
struct kdtree{
	int mn[2], mx[2], d[2];
	int ls, rs, val;
	kdtree() {
		ls = rs = val = d[0] = d[1] = 0;
		mn[0] = mn[1] = inf;
		mx[0] = mx[1] = 0;
	}
}kdt[N];
bool cmp(const kdtree &a, const kdtree &b) {
	return a.d[type] < b.d[type];
}
void build(int L, int R, int k, int &cur) {
	int mid = (L + R) >> 1;
	cur = mid;
	type = k;
	nth_element(kdt + L + 1, kdt + mid + 1, kdt + R + 1, cmp);
	if (mid - 1 >= L)	build(L, mid - 1, k ^ 1, kdt[mid].ls);
	if (mid + 1 <= R) build(mid + 1, R, k ^ 1, kdt[mid].rs);
	pushup(mid);//依据题意写
}

K-D Tree的复杂度

K-D Tree实际上是一种暴力的优化算法,它的使用就是暴力加剪枝,但复杂度竟然是n^(1+(1 - 1/k))(k维)

K-D Tree的使用(例题)

P4475 巧克力王国

以x和y为横纵坐标,建立平面直角坐标系。注意到对于每个询问,其符合要求的范围是连续的。确切地说,其范围应该是一条直线的左端。建一颗K-D Tree,然后从根节点开始找,如果某节点全部符合要求或全部不符合要求,就直接把它剪掉,不往下递归。这需要我们维护各节点的美味度总和.

部分代码:

inline void pushup(int cur) {
	register int ls = kdt[cur].ls, rs = kdt[cur].rs;
	kdt[cur].sum = kdt[ls].sum + kdt[rs].sum + kdt[cur].val;
	for (register int i = 0; i <= 1; ++i) {
		kdt[cur].mx[i] = kdt[cur].mn[i] = kdt[cur].d[i];
		if (ls) {
			kdt[cur].mn[i] = min(kdt[ls].mn[i], kdt[cur].mn[i]);
			kdt[cur].mx[i] = max(kdt[ls].mx[i], kdt[cur].mx[i]);
		}
		if (rs) {
			kdt[cur].mn[i] = min(kdt[rs].mn[i], kdt[cur].mn[i]);
			kdt[cur].mx[i] = max(kdt[rs].mx[i], kdt[cur].mx[i]);
		}
	}
}
...
inline bool che(int x, int y) {
	return aaa * x + bbb * y < ccc;
}
int query(int cur) {
	int res = 0, cnt = 0;
	cnt += che(kdt[cur].mn[0], kdt[cur].mn[1]);
	cnt += che(kdt[cur].mn[0], kdt[cur].mx[1]);
	cnt += che(kdt[cur].mx[0], kdt[cur].mn[1]);
	cnt += che(kdt[cur].mx[0], kdt[cur].mx[1]);
	if (cnt == 4)	return kdt[cur].sum;
	if (!cnt)	return 0;
	if (che(kdt[cur].d[0], kdt[cur].d[1]))	res += kdt[cur].val;
	if (kdt[cur].ls)	res += query(kdt[cur].ls);
	if (kdt[cur].rs)	res += query(kdt[cur].rs);
	return res;
}

P4357 [CQOI2016]K远点对

搞个小根堆,维护最大的那k个点对。

由于K-D Tree 的剪枝像大多数 DFS 的剪枝一样,它并不需要一些准确的信息,只要“最优”情况不能更新答案,就可以剪掉它。不过更新答案的时候是要用准确信息的。

这道题的“最优”情况为矩形的四条边(甚至都可能不是一个点)的坐标。

加强版:P2093 [国家集训队]JZPFAR

我做的第一道国集JZP题

其实这种问题还能优化。查询的时候,如果发现左儿子的最优假答案比右儿子的最优假答案更优的话,那么我们要先去左儿子,再去右儿子。这样,我们先获得了更接近最优答案的答案,以后就能剪掉更多的枝了。

这个剪枝是个 K-D Tree 的经典套路。这道题(JZPFAR)不这么剪还有一半分,P2479 [SDOI2010]捉迷藏就只有30分了。

关键代码:

//pr = pair,一开始想用pair水过,后来还是写的结构体
inline ll get_dis(int x, int y, int X, int Y) {
	return Pow(x - X) + Pow(y - Y);
}
inline ll fake_dis(node nd, int x, int y) {
	return max(Pow(nd.mx[0] - x), Pow(nd.mn[0] - x)) + 
		max(Pow(nd.mx[1] - y), Pow(nd.mn[1] - y));
}
void query(int x, int y, int cur) {
	if (!cur)	return ;
	ll tmp = get_dis(nd[cur].d[0], nd[cur].d[1], x, y);
	Node pr = (Node){tmp, nd[cur].id};
	if (pr < q.top())	q.pop(), q.push(pr);
	int ls = nd[cur].ls, rs = nd[cur].rs;
	ll dl, dr;
	if (ls) dl = fake_dis(nd[ls], x, y);
	else	dl = -1;
	if (rs)	dr = fake_dis(nd[rs], x, y);
	else	dr = -1;
	Node Pl = (Node){dl, nd[ls].id}, Pr = (Node){dr, nd[rs].id};
	if (Pl < Pr) {
		if (Pl < q.top())	query(x, y, ls);
		if (Pr < q.top())	query(x, y, rs);
	} else {
		if (Pr < q.top())	query(x, y, rs);
		if (Pl < q.top())	query(x, y, ls);
	}
}

P4148 简单题

二维平面中单点加,矩形数点。强制在线。\(q <= 2e5\).空间20MB,时间8s.

由于强制在线且卡空间,这题 K-D Tree 成为比较理想的解法。

由于不断加点可能导致不平衡,我们需要不时地重构一下。或者像替罪羊树那样搞一个 \(alpha\) 值。

void Build(int L, int R, int d, int &cur) {
	if (L > R)	return cur = 0, void();
	int mid = (L + R) >> 1;
	nwd = d;
	nth_element(stk + L, stk + mid, stk + R, cmp);
	nd[cur] = stk[mid];
	Build(L, mid, d ^ 1, nd[cur].ls);
	Build(mid + 1, R, d ^ 1, nd[cur].rs);
	pushup(cur);
}
inline bool che(int cur) {
	int ls = nd[cur].ls;
	return nd[ls].siz / nd[cur].siz >= alpha;
}
inline void Rebuild(int &cur) {
	stop = 0;
	Del(cur);
	Build(1, stop, 0, cur);
}
void add(int d, int &cur) {
	if (!cur) {
		cur = ++ttot;
		nd[cur] = tp;
		return ;
	}
	if (tp.d[d] < nd[cur].d[d])	add(d ^ 1, nd[cur].ls);
	else	add(d ^ 1, nd[cur].rs);
	pushup(cur);
	if (che(cur))	Rebuild(cur);
}

P5471 [NOI2019]弹跳

又是卡空间,需要 K-D Tree 优化建图。不过还不行,不能显式地建出边,直接在 K-D Tree 的框架下模拟 Dijkstra 算法流程才行。不用剪枝可过 loj,需要剪枝才能过洛谷,剪枝也不能过 uoj。

inline void Update(int cur, int v) {
	if (v < dis[cur])
		dis[cur] = v, q.push((Node){cur, dis[cur]});
}
void modify(int l, int r, int d, int u, int v, int cur) {
	if (!cur || v >= dis[cur + n])	return ;
	int al = nd[cur].mn[0], ar = nd[cur].mx[0], ad = nd[cur].mn[1], au = nd[cur].mx[1];
	if (ar < l || al > r || au < d || ad > u)	return ;
	if (l <= al && ar <= r && d <= ad && au <= u)	return Update(cur + n, v), void();
	int x = nd[cur].d[0], y = nd[cur].d[1];
	if (l <= x && x <= r && d <= y && y <= u)	Update(cur, v);
	modify(l, r, d, u, v, nd[cur].ls);
	modify(l, r, d, u, v, nd[cur].rs);
}
int mp[N];
inline void dij() {
	memset(dis, 0x3f, sizeof(dis));
	dis[mp[1]] = 0;
	q.push((Node){mp[1], 0});//Attention!!
	while (!q.empty()) {
		Node Nd = q.top(); q.pop();
		int cur = Nd.cur;
		if (vis[cur])	continue;
		vis[cur] = true;
		if (cur <= n) {
			for (register unsigned int i = 0; i < mt[cur].size(); ++i) {
				matrix mat = mt[cur][i];
				modify(mat.l, mat.r, mat.d, mat.u, dis[cur] + mat.t, root);
			}
		} else {
			cur -= n;
			node nod = nd[cur];
			int ls = nod.ls, rs = nod.rs;
			Update(cur, dis[cur + n]);
			if (ls)	Update(ls + n, dis[cur + n]);//Attention!!!
			if (rs)	Update(rs + n, dis[cur + n]);//Attention!!!
		}
	}
}

注意

  • 那个 nth_element 的顺序是:左 + 1,中 + 1,右 + 1,cmp。注意加一(尽管之前有些代码没加一也能过)

又查了一遍cplusplus reference,nth_element(first, nth, last) 是对 [first,last) 进行排序,只不过只保证 [first,nth) <= nth <= (nth,last)。但是实际测试中发现它往往会把 nth 附近的也搞成有序的了。

因此排序应该写成:nth_element(nd + L, nd + mid, nd + R + 1)

  • 由于排序,建完树后每个节点的下标和原标号有所不同。手写 map 映射一下即可。
posted @ 2020-07-23 16:33  JiaZP  阅读(196)  评论(0编辑  收藏  举报