瞎扯
\(\text{K-Dimensional Tree}\) 簡稱 \(\text{K-D Tree}\),是一種 \(K\) 維的二叉搜索樹。能實現的功能有:
- 查找最近/最遠點對
- 矩形數點
基本操作
建樹
\(\text{K-D Tree}\) 中每個節點需要維護一個 \(K\) 維空間,故需要維護:\(K\) 維空間中的一個點、該節點在 \(K\) 維空間中表示的區域,左/右兒子分別是哪個點。一般使用如下結構體:
struct point {
int lson, rson;
ll d[K], mn[K], mx[K];
// d[] 該節點代表的點的座標,mn[] 存該點代表的空間中的點的每一維的最小值,mx[] 則是最大值
friend bool operator < (point p1, point p2) {
// 建樹操作需要用到比較大小,現在不理解就繼續往下看
if (p1.d[f] ^ p2.d[f]) return p1.d[f] < p2.d[f];
for (int i = 0; i < K; ++i) {
if (p1.d[i] ^ p2.d[i]) return p1.d[i] < p2.d[i];
}
return true;
}
}tree[M];
每個節點維護了一個 \(K\) 維的空間。普通的二叉搜索樹的建樹方式爲一個節點左邊的節點都比該節點小,該節點右邊的節點都比該節點大,即有一個判斷兩點大小的標準。在 \(\text{K-D Tree}\) 中常用如下方式進行建樹:
一般根節點深度爲 \(0\),深度爲 \(0\) 的按第 \(1\) 維座標大小分,深度爲 \(1\) 的按第 \(2\) 維座標大小分,深度爲 \(d\) 的按 \((d\bmod K) +1\) 維座標的大小分。爲了使建出來的樹儘量平衡(左右儘量節點數一樣)以保證優秀的時間複雜度,每次選擇該維上的中位數來分,選的這個中位數就是一個根節點。
這裏就需要用到 <algorithm>
中的 std::nth_element(a + l, a + mid, a + r + 1)
,作用是將 a 數組中的 \([l,r]\) 這一段中第 \(mid - l + 1\) 小的元素放到 \(mid\) 的位置上,並保證左邊的數都比 \(mid\) 小,右邊的數都比 \(mid\) 大。時間複雜度是 \(O(n)\),因爲按照如上方式建樹樹高在 \(\log n\) 級別上,每一層只需要調用一次該函數,所以建樹的複雜度是 \(O(n\log n)\)。建樹代碼如下:
void pushup(int u, int v) {
for (int i = 0; i < K; ++i) {
tree[u].mn[i] = min(tree[u].mn[i], tree[v].mn[i]);
tree[u].mx[i] = max(tree[u].mx[i], tree[v].mx[i]);
}
}
int build(int l, int r, int t) {
if (l > r) return 0;
int mid = (l + r) >> 1; f = t;//這個 f 和上面結構體中的 f 都代表按哪一維劃分。
std::nth_element(tree + l, tree + mid, tree + r + 1);
for (int i = 0; i < K; ++i) {
tree[mid].mn[i] = tree[mid].mx[i] = tree[mid].d[i];
}
tree[mid].lson = build(l, mid - 1, (t + 1) % K);
tree[mid].rson = build(mid + 1, r, (t + 1) % K);
if (tree[mid].lson) pushup(mid, tree[mid].lson);
if (tree[mid].rson) pushup(mid, tree[mid].rson);
return mid;
}
插入
要插入 \((x,y)\),就從根節點開始,按照這一層的劃分標準來判斷是插到左子樹中還是右子樹中。代碼如下:
void ins(int now, int x, int y, int t) {
if (t ? tree[now].d[t] >= y : tree[now].d[t] >= x) {
if (tree[now].lson) ins(tree[now].lson, x, y, (t + 1) % K);
else {
tree[now].lson = ++n;
tree[n].d[0] = x, tree[n].d[1] = y;
for (int i = 0; i < K; ++i) {
tree[n].mn[i] = tree[n].mx[i] = tree[n].d[i];
}
}
pushup(now, tree[now].lson);
} else {
if (tree[now].rson) ins(tree[now].rson, x, y, (t + 1) % K);
else {
tree[now].rson = ++n;
tree[n].d[0] = x, tree[n].d[1] = y;
for (int i = 0; i < K; ++i) {
tree[n].mn[i] = tree[n].mx[i] = tree[n].d[i];
}
}
pushup(now, tree[now].rson);
}
}
但是這樣有個問題,經過精心構造的數據能夠在多次插入後形成一條很長的鏈,使得樹高變大導致時間複雜度退化。因此需要下面的重構操作。
重構
還不會,咕着。
一個針對離線插入點的小 Trick
從這裏學的,一開始建樹的時候把所有的點都放到樹上(包括之後的操作中插入的)利用標記來表示當前這個點是否被插入,對代碼感興趣可以先看這個提交記錄,有時間再詳細寫。
常用操作
查詢最近/最遠距離
下面以最近距離爲例。
估價函數
用於計算出當前位置到目標區域的距離的上下界。
上界:走到目標區域的某一個角上。
下界:走到目標區域的某一條邊上(在矩形內部下界爲 \(0\))
曼哈頓距離
從 \((x1,y1)\) 到 \((x2,y2)\) 的距離爲 \(|x1-x2|+|y1-y2|\)。估價函數如下:
// (x,y) 到矩形 now 的最小曼哈頓距離的估價函數(走到邊上)
max(abs(x - tree[now].mn[0]), abs(tree[now].mx[0] - x)) + max(abs(y - tree[now].mn[1]), abs(tree[now].mx[1] - y));
// (x,y) 到矩形 now 的最大曼哈頓距離的估價函數(走到四個角上)
max(tree[now].mn[0] - x, 0) + max(x - tree[now].mx[0], 0) + max(tree[now].mn[1] - y, 0) + max(y - tree[now].mx[1], 0);
歐幾里得距離
從 \((x1,y1)\) 到 \((x2,y2)\) 的距離爲 \((x1-x2)^2+(y1-y2)^2\)。估價函數如下:
//sqr(x) 求的是 x * x
// (x,y) 到矩形 now 的最小歐幾里得距離的估價函數(走到邊上)
sqr(max(tree[now].mn[0] - x, 0)) + sqr(max(x - tree[now].mx[0], 0)) + sqr(max(tree[now].mn[1] - y, 0)) + sqr(max(y - tree[now].mx[1], 0));
// (x,y) 到矩形 now 的最大歐幾里得距離的估價函數(走到四個角上)
max(sqr(tree[now].mn[0] - x), sqr(x - tree[now].mx[0])) + max(sqr(tree[now].mn[1] - y), sqr(y - tree[now].mx[1]));
查詢
從樹的根節點開始,先將樹的根節點到詢問點的距離作爲最小距離,然後使用估價函數(提到估價函數有木有覺得很玄學)估計左右兩顆子樹的最小值,判斷是否需要進入子樹中進行查找,如果需要進入子樹中查找則先進入估計值小的。比較像使用估價函數對一次搜索進行了剪枝,效率如何在於估價函數。代碼如下:
void query_mn(int now, int x, int y) {
int temp = abs(tree[now].d[0] - x) + abs(tree[now].d[1] - y);
if (temp < minn) minn = temp; //先暫定根節點
//需要根據題意判斷能否爲兩個相同的點之間的距離,也就是 0
int templ = tree[now].lson ? fmn(tree[now].lson, x, y) : inf;
int tempr = tree[now].rson ? fmn(tree[now].rson, x, y) : inf;
// fmn() 函數就是估價函數,對左右子樹進行估價,然後判斷是否去子樹裏搜索
if (templ < tempr) {
if (templ < minn) query_mn(tree[now].lson, x, y);
if (tempr < minn) query_mn(tree[now].rson, x, y);
} else {
if (tempr < minn) query_mn(tree[now].rson, x, y);
if (templ < minn) query_mn(tree[now].lson, x, y);
}
}