Day 068-Q2 — 平面上の最近点対問題(分割統治 $O(N \log N)$)

2026-06-21 赤色 Master / Phase 8+ ★★★★★★★★★ Closest Pair of Points・分割統治・整数距離

問題

平面上に $N$ 点 $(x_i, y_i)$ が与えられる。$Q$ 個のクエリに対し、先頭 $k$ 点($k$ はクエリで与えられる)における最近点対のユークリッド距離の2乗を出力せよ。

制約

パラメータ範囲
$N$$2 \le N \le 2 \times 10^5$
$Q$$1 \le Q \le 2 \times 10^5$
$k_i$$2 \le k_i \le N$
$x_i, y_i$$-10^9 \le x_i, y_i \le 10^9$
同一座標存在しない

入出力例

入力例 1

5 3
0 0
3 4
1 1
6 1
4 3
2
3
5

出力例 1

25
2
2

k=2: (0,0),(3,4) → $9+16=25$。k=3: (0,0),(1,1) → $1+1=2$。k=5: (0,0)-(1,1) or (1,1)-(0,0) → 2。

概念図: 分割統治法の「中線ストリップ」

左半分 (x < mid_x) P1 P2 P3 右半分 (x ≥ mid_x) mid_x strip幅 = sqrt(d) d_L d_R d = min(d_L, d_R) → ストリップ内で最大7点と比較 → 全体 O(N log N)

ヒント(段階的開示)

ヒント1: 方向性
全点に対する最近点対は分割統治で $O(N \log^2 N)$(ストリップソートを毎回すると)または $O(N \log N)$(マージソートと統合)で解ける。整数座標を使い距離の2乗で管理することで浮動小数点誤差を完全に回避する。
ヒント2: アプローチ
  • x 座標でソート後、中央で左右分割
  • $d = \min(d_L, d_R)$ を求め、中線から $\sqrt{d}$ 以内の点のみをストリップとして抽出
  • ストリップを y 座標順にスキャン、y差が $d$ を超えたら打ち切り(最大7点と比較)
  • 距離の2乗 $(dx^2 + dy^2)$ で管理して整数演算のみ使用
ヒント3: コード骨格
def closest_rec(pts):
    n = len(pts)
    if n <= 3:
        return min(dist2(pts[i],pts[j])
                   for i in range(n) for j in range(i+1,n))
    mid = n // 2
    mid_x = pts[mid][0]
    d = min(closest_rec(pts[:mid]), closest_rec(pts[mid:]))
    # ストリップ: (x - mid_x)^2 < d
    strip = sorted([p for p in pts if (p[0]-mid_x)**2 < d],
                   key=lambda p: p[1])
    i = 0
    while i < len(strip):
        j = i + 1
        while j < len(strip) and (strip[j][1]-strip[i][1])**2 < d:
            d = min(d, dist2(strip[i], strip[j]))
            j += 1
        i += 1
    return d

模範解答 (Python)

import sys
input = sys.stdin.readline

def dist2(a, b):
    return (a[0]-b[0])**2 + (a[1]-b[1])**2

def closest_strip(strip, d):
    n = len(strip)
    for i in range(n):
        j = i + 1
        while j < n and (strip[j][1] - strip[i][1])**2 < d:
            d = min(d, dist2(strip[i], strip[j]))
            j += 1
    return d

def closest_rec(pts):
    n = len(pts)
    if n <= 3:
        best = float('inf')
        for i in range(n):
            for j in range(i+1, n):
                best = min(best, dist2(pts[i], pts[j]))
        return best
    mid = n // 2
    mid_x = pts[mid][0]
    d = min(closest_rec(pts[:mid]), closest_rec(pts[mid:]))
    strip = []
    for p in pts:
        if (p[0] - mid_x)**2 < d:
            strip.append(p)
    strip.sort(key=lambda p: p[1])
    return closest_strip(strip, d)

def main():
    sys.setrecursionlimit(200000)
    N, Q = map(int, input().split())
    pts = []
    for _ in range(N):
        x, y = map(int, input().split())
        pts.append((x, y))

    queries = []
    for qi in range(Q):
        k = int(input())
        queries.append((k, qi))

    ans = [0] * Q
    for k, qi in queries:
        sub = sorted(pts[:k], key=lambda p: p[0])
        ans[qi] = closest_rec(sub)

    print('\n'.join(map(str, ans)))

main()

Step-by-Step 解説

Step 1: 基本的な分割統治

x座標でソート後、中央で左右に分割。左右それぞれ再帰的に最近点対距離 $d_L$, $d_R$ を求め、$d = \min(d_L, d_R)$ を得る。

Step 2: ストリップの処理

中線 $x = m$ から $\sqrt{d}$ 以内の点だけがクロス最近点対の候補。y座標でソートし、y差が $d$(2乗)を超えたら比較を打ち切る。幾何学的に各点の比較相手は最大7点以下であることが証明されており、ストリップ処理全体で $O(N)$。

Step 3: 整数演算の徹底

ストリップ条件 (p[0]-mid_x)**2 < d と y比較 (strip[j][1]-strip[i][1])**2 < d をいずれも距離の2乗で比較することで、sqrt を一切使わず浮動小数点誤差ゼロを実現。

Step 4: 計算量

ステップ計算量
初期ソート$O(N \log N)$
再帰(ストリップを毎回ソート)$O(N \log^2 N)$
マージソート統合版$O(N \log N)$
クエリ全体$O(Q \cdot k \log^2 k)$ 最悪

よくあるミス

ミス原因正しい書き方
浮動小数点比較sqrt の誤差距離の2乗のまま比較する
ストリップ条件に sqrt 使用誤差で候補を落とす(p[0]-mid_x)**2 < d
再帰基底を n≤1 にペアが存在しないn≤3 で全探索する
y比較も2乗を忘れるdy < sqrt(d) と書くdy**2 < d で統一

次のステップ

発展問題: 動的点追加で最近点対を維持する(Randomized Incremental または KD-Tree による動的版)。3次元空間への拡張。

自己評価