「学习笔记」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);
  }
}

参考资料

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