「學習筆記」K-DTree

瞎扯

\(\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);
  }
}

參考資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章