1. 程式人生 > 實用技巧 >K-D Tree

K-D Tree

K-D Tree可以搞多維空間問題,其形式為一棵二叉搜尋樹,它能把一張高維(>=2)的圖分成好多塊,其節點的某維座標大於其左兒子,小於其右兒子。

K-D Tree的建立

對於k維的問題,第i層我們根據區間內各點的第(i % k)維座標,用快速排序的思想,可以做到 log 時間內找中位數,然後以中位數的節點為當前根,把當前區間一分為二,然後(如果有的話)遞迴到其左右區間,左右區間的根即為當前根的左右兒子。如圖:

【卡常技巧】最好把 \(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。注意加一(儘管之前有些程式碼沒加一也能過)

  • 由於排序,建完樹後每個節點的下標和原標號有所不同。手寫 map 對映一下即可。