Day 067-Q5 — 動的平面最近傍探索(KD-Tree + オンライン点追加)

2026-06-20 赤色 Master / Phase 8+ ★★★★★★★★★ Dynamic KD-Tree + Logarithmic Rebuilding

問題

$Q$ 個のオンラインクエリを処理せよ。

  • クエリ型 1: 1 x y — 点 $(x, y)$ を集合に追加する。
  • クエリ型 2: 2 x y — 集合内の点で $(x, y)$ に最も近い点のユークリッド距離の2乗を求めよ。集合が空なら -1 を出力せよ。

制約

パラメータ範囲
$Q$$1 \le Q \le 10^5$
$x, y$$0 \le x, y \le 10^9$

入出力例

入力例 1

6
1 3 4
1 1 1
2 2 2
1 7 8
2 5 5
2 0 0

出力例 1

2
8
2

(2,2)→(1,1)距離^2=2。(5,5)→(3,4)距離^2=8。(0,0)→(1,1)距離^2=2。

概念図: KD-Tree + Logarithmic Rebuilding

KD-Tree 構造と最近傍探索の枝刈り KD-Tree(2D点の空間分割) 中央値で X 軸分割 左: Y 軸分割 右: Y 軸分割 葉 (x,y) 葉 (x,y) 各ノードにバウンディングボックスを保持 クエリ点からの最小距離 ≥ 現最近傍 → 枝刈り Logarithmic Rebuilding 複数の静的KD-Treeを管理 サイズ 1, 2, 4, 8, ... 個の点を持つ 1 2 4 8 二進数のキャリー操作と同じ合体 追加: O(log² N) amortized クエリ: O(log N × √N) 期待 全ツリーを走査して最小距離を返す バウンディングボックス枝刈りの例 クエリ点 探索 スキップ 現在の最近傍距離の円

ヒント(段階的開示)

ヒント1: 方向性
KD-Tree を使った最近傍探索。静的 KD-Tree は $O(N \log N)$ 構築・$O(\sqrt{N})$ 平均クエリ。オンライン追加には「Logarithmic Rebuilding」を使う。
ヒント2: アプローチ

Logarithmic Rebuilding: サイズ $2^k$ の静的 KD-Tree を複数管理。追加時は同サイズのツリーを合体(二進数のキャリー操作と同様)。$O(\log^2 N)$ 平均追加。

最近傍クエリ: 各ツリーを走査し全ツリーの最小距離を返す。

ヒント3: コード骨格
class LogKDTree:
    def __init__(self):
        self.trees = {}  # size -> (kdtree, points)

    def add(self, x, y):
        size = 1
        pts = [(x, y)]
        while size in self.trees:
            old = self.trees.pop(size)[1]
            pts += old
            size *= 2
        self.trees[size] = (self._build(pts, 0), pts)

    def nearest(self, qx, qy):
        best = inf
        for size, (tree, _) in self.trees.items():
            best = self._query(tree, qx, qy, best)
        return best if best < inf else -1

模範解答 (Python)

import sys
from math import inf
input = sys.stdin.readline

class LogKDTree:
    def __init__(self):
        self.trees = {}
        self.total = 0

    def _build(self, pts, depth):
        if not pts:
            return None
        axis = depth % 2
        pts.sort(key=lambda p: p[axis])
        mid = len(pts) // 2
        node = [pts[mid], None, None,
                min(p[0] for p in pts), max(p[0] for p in pts),
                min(p[1] for p in pts), max(p[1] for p in pts)]
        node[1] = self._build(pts[:mid], depth + 1)
        node[2] = self._build(pts[mid+1:], depth + 1)
        return node

    def _box_dist(self, node, qx, qy):
        dx = max(0, node[3] - qx, qx - node[4])
        dy = max(0, node[5] - qy, qy - node[6])
        return dx * dx + dy * dy

    def _query_tree(self, node, qx, qy, best):
        if node is None:
            return best
        px, py = node[0]
        d = (px - qx) ** 2 + (py - qy) ** 2
        best = min(best, d)
        children = []
        if node[1]: children.append((node[1], self._box_dist(node[1], qx, qy)))
        if node[2]: children.append((node[2], self._box_dist(node[2], qx, qy)))
        children.sort(key=lambda x: x[1])
        for child, bd in children:
            if bd < best:
                best = self._query_tree(child, qx, qy, best)
        return best

    def add(self, x, y):
        size = 1
        pts = [(x, y)]
        while size in self.trees:
            old_pts = self.trees.pop(size)[1]
            pts = pts + old_pts
            size *= 2
        self.trees[size] = (self._build(list(pts), 0), pts)
        self.total += 1

    def nearest(self, qx, qy):
        if self.total == 0:
            return -1
        best = inf
        for size, (tree, pts) in self.trees.items():
            best = self._query_tree(tree, qx, qy, best)
        return best if best < inf else -1

def main():
    sys.setrecursionlimit(400010)
    Q = int(input())
    kd = LogKDTree()
    out = []
    for _ in range(Q):
        line = list(map(int, input().split()))
        if line[0] == 1:
            _, x, y = line
            kd.add(x, y)
        else:
            _, x, y = line
            out.append(kd.nearest(x, y))
    print('\n'.join(map(str, out)))

main()

Step-by-Step 解説

Step 1: 静的 KD-Tree の構築

各次元を交互に分割軸とし、中央値で再帰的に分割する。各ノードにバウンディングボックス(min/max x, min/max y)を保持。

Step 2: 最近傍クエリの枝刈り

クエリ点からノードのバウンディングボックスへの最小距離 $\ge$ 現在の最近傍距離なら枝刈り。近い方の子を先に探索する。

Step 3: Logarithmic Rebuilding

サイズ $2^k$ の静的 KD-Tree を複数管理。追加時は同サイズのツリーを合体(二進数のキャリー操作と同様)。$O(\log N)$ 回再構築、合計 $O(N \log^2 N)$。

Step 4: 計算量

操作計算量
点追加(amortized)$O(\log^2 N)$
最近傍クエリ(期待値)$O(\log N \cdot \sqrt{N})$
全体$O(N \log^2 N + Q \log N \sqrt{N})$

よくあるミス

ミス原因正しい書き方
バウンディングボックス更新漏れ build時にmin/maxを正しく計算しない 全子点のmin/maxを格納
枝刈り条件の符号ミス >= と > の混同 bd < best の場合のみ再帰
空ツリーへのクエリ total=0のとき-1を返すべき if self.total == 0: return -1

次のステップ

発展: BBD-Tree(Box Decomposition Tree)による最近傍クエリ $O(\log N)$。関連: 静的 KD-Tree + オフライン再構築(バッチ追加問題)。

自己評価