Day 130-Q5 — 最近点対問題(分割統治法)

2026-08-22 赤色 Master / Phase 8+ ★★★★★★★★★ x座標分割+strip上のy座標絞り込み

問題

平面上に$N$個の相異なる点が与えられる。すべての点対の中で、ユークリッド距離の2乗が最小となる値を求めよ。

入力形式

N
x_1 y_1
x_2 y_2
...
x_N y_N

制約

$2 \le N \le 200000$
$0 \le x_i, y_i \le 10^9$

入出力例

入力例1

6
2 3
12 30
40 50
5 1
12 10
3 4

出力例1

2

点(2,3)と(3,4)の距離2乗は(3-2)²+(4-3)²=1+1=2で全点対中最小。

概念図

x座標で二分し、境界の帯(strip)だけを絞り込む 左半分 (d_l) 右半分 (d_r) 帯 strip: |x-midx| < √d の点だけ抽出 帯をy座標でソートし、各点から後続高々6〜7個だけ距離を確認すればよい

ヒント(段階的開示)

ヒント1(方向性)

全点対総当たりはO(N²)。x座標でソートし左右に分割、それぞれ再帰的に最近点対を求める分割統治の骨格はすぐ思いつく。問題は分割線をまたぐ点対の扱い。

ヒント2(アプローチ)

左右の最小距離2乗をdとする。分割線から横方向の距離が√d未満の点だけを集めた帯(strip)を作れば答えを更新しうる点対はこの帯の中にしかない。帯の中をy座標でソートすればy差が√d未満の相手は定数個(高々7個程度)しかないことが証明できる。

ヒント3(誘導)
def rec(pts_x):
    if len(pts_x) <= 3:
        return brute(pts_x)
    mid = len(pts_x) // 2
    midx = pts_x[mid][0]
    dl = rec(pts_x[:mid])
    dr = rec(pts_x[mid:])
    d = min(dl, dr)
    strip = [p for p in pts_x if (p[0] - midx) ** 2 < d]
    strip.sort(key=lambda p: p[1])
    for i in range(len(strip)):
        for j in range(i + 1, min(i + 7, len(strip))):
            d = min(d, dist2(strip[i], strip[j]))
    return d

模範解答 (Python)

import sys


def solve():
    data = sys.stdin.read().split()
    idx = 0
    n = int(data[idx]); idx += 1
    pts = []
    for _ in range(n):
        x = int(data[idx]); y = int(data[idx + 1]); idx += 2
        pts.append((x, y))

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

    def brute(pts_list):
        best = float('inf')
        m = len(pts_list)
        for i in range(m):
            for j in range(i + 1, m):
                d = dist2(pts_list[i], pts_list[j])
                if d < best:
                    best = d
        return best

    pts_x = sorted(pts)

    def rec(lo, hi):
        m = hi - lo
        if m <= 3:
            return brute(pts_x[lo:hi])
        mid = lo + m // 2
        midx = pts_x[mid][0]
        dl = rec(lo, mid)
        dr = rec(mid, hi)
        d = min(dl, dr)

        strip = [p for p in pts_x[lo:hi] if (p[0] - midx) ** 2 < d]
        strip.sort(key=lambda p: p[1])
        sm = len(strip)
        for i in range(sm):
            for j in range(i + 1, min(i + 7, sm)):
                dd = dist2(strip[i], strip[j])
                if dd < d:
                    d = dd
        return d

    print(rec(0, n))


solve()

Step-by-Step 解説

1x座標での分割
点をx座標でソートし中央で左右に分ける。それぞれ再帰的に最近点対距離2乗を求める。点数3以下は総当たり。
2分割線をまたぐペアの絞り込み
d=min(dl,dr)が分かれば、更新しうるのは分割線からの水平距離が√d未満の点だけ。これをstripとして抽出。
3y座標ソートの理由
帯の中の点をy座標でソートしておくことで近い候補を連続範囲に集約できる。
4内側ループを定数回で打ち切れる根拠
帯の幅は2√dしかなく、鳩の巣原理により同じ半分の点が同じd×d/2領域に2つ以上入ると矛盾する。よって高々6〜7個で打ち切ってよい。
5計算量
T(N)=2T(N/2)+O(N log N)(帯のソート分)。y座標ソート済み配列をマージするようにすればO(N)に落とせ全体O(N log N)。本解答は簡単のためO(N log²N)だがN≤2×10^5なら十分高速。

よくあるミス

ミス原因正しい書き方
距離そのもの(平方根あり)で比較する浮動小数点誤差が混入し厳密比較が壊れる座標が整数なら距離の2乗のまま整数演算で比較し続ける
内側ループを絞らず帯を全探索する定数個で打ち切れる根拠を知らないmin(i+7, sm)のように打ち切りを入れて計算量を担保する
帯の判定で距離2乗と生の距離を混同する単位が揃っていない比較(p[0]-midx)**2 < dのように両辺を2乗のスケールで揃える
基底ケースで点数2以下の時のminでエラー境界条件の考慮漏れ点数3以下は常にbruteに委ね内部でinfを返すようにする

次のステップ

  • 発展: 3次元以上に拡張した最近点対問題も同様の分割統治+帯絞り込みで解ける
  • 次回予告: 次回のテーマは今後のセッションで選定

自己評価

自分の回答

気づき・メモ