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)不這麼剪還有一半分,
關鍵程式碼:
//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 對映一下即可。