Day 073-Q3 — 凸多角形の接線・最近点クエリ(二分探索 + 回転キャリパー)

2026-06-26 赤色 Master / Phase 8+ ★★★★★★★★★ 計算幾何・外積・内外判定・最近点

問題

$N$ 頂点の凸多角形(反時計回りに与えられる)と $Q$ 個の点クエリ $q_i = (x_i, y_i)$ が与えられる。

各クエリに対して、以下を答えよ:

  1. 内外判定: 点 $q_i$ が凸多角形の内部(辺上を含む)にあるか外部にあるか
  2. 最近点距離: 点 $q_i$ から凸多角形の境界までのユークリッド距離(内部の場合は 0)

制約

パラメータ範囲備考
$N$$3 \le N \le 10^5$頂点数
$Q$$1 \le Q \le 10^5$クエリ数
座標整数、$|x|, |y| \le 10^9$
凸性を厳密に満たす(3点非共線)

入出力例

入力例1

4 3
0 0
4 0
4 4
0 4
2 2
5 2
0 0

出力例1

INSIDE 0.000000
OUTSIDE 1.000000
INSIDE 0.000000

概念図: 扇形分割による内外判定 $O(\log N)$

P₀ S1 S2 S3 S4 S5 q (内部) q (外部) 最近点 アルゴリズム: 1. P₀から見てq の角度で二分探索 → セクター特定 O(log N) 2. セクター内の辺との外積で内外判定 → O(1)

ヒント

ヒント1(方向性)

凸多角形の内外判定は $O(\log N)$ で行える。頂点 0 を基準に「扇形分割」し、クエリ点がどの扇形セクターにあるかを二分探索で特定する。

ヒント2(アプローチ)

内外判定:

  1. 頂点 $P_0$ を起点とし、クエリ点 $q$ の相対ベクトルを作る
  2. $q$ が $\vec{P_0P_1}$ と $\vec{P_0P_{N-1}}$ の間の角度範囲内かチェック
  3. 二分探索でセクター特定 → セクター内の辺との外積で最終判定

最近点距離:

外部なら全 $N$ 辺との距離を計算し最小値を取る $O(N)$。$N \le 10^5$ なので許容。

ヒント3(ほぼ答え)
def cross2d(o, a, b):
    return (a[0]-o[0])*(b[1]-o[1]) - (a[1]-o[1])*(b[0]-o[0])

def inside_convex(poly, q):
    n = len(poly)
    p0 = poly[0]
    c1 = cross2d(p0, poly[1], q)
    cn = cross2d(p0, poly[-1], q)
    if c1 < 0 or cn > 0:
        return False
    lo, hi = 1, n-1
    while lo+1 < hi:
        mid = (lo+hi)//2
        if cross2d(p0, poly[mid], q) >= 0:
            lo = mid
        else:
            hi = mid
    return cross2d(poly[lo], poly[lo+1 if lo+1 < n else 0], q) >= 0

模範解答

import sys, math
input = sys.stdin.readline

def cross2d(o, a, b):
    return (a[0]-o[0])*(b[1]-o[1]) - (a[1]-o[1])*(b[0]-o[0])

def seg_dist(p, a, b):
    ax, ay = a; bx, by = b; px, py = p
    abx, aby = bx-ax, by-ay
    apx, apy = px-ax, py-ay
    ab2 = abx*abx + aby*aby
    if ab2 == 0:
        return math.hypot(apx, apy)
    t = max(0.0, min(1.0, (apx*abx + apy*aby) / ab2))
    return math.hypot(px - (ax + t*abx), py - (ay + t*aby))

def inside_convex(poly, q):
    n = len(poly)
    p0 = poly[0]
    c1 = cross2d(p0, poly[1], q)
    cn = cross2d(p0, poly[-1], q)
    if c1 < 0 or cn > 0:
        return False
    lo, hi = 1, n-1
    while lo+1 < hi:
        mid = (lo+hi)//2
        if cross2d(p0, poly[mid], q) >= 0:
            lo = mid
        else:
            hi = mid
    nxt = lo+1 if lo+1 < n else 0
    return cross2d(poly[lo], poly[nxt], q) >= 0

def min_dist_to_convex(poly, q):
    n = len(poly)
    if inside_convex(poly, q):
        return 0.0
    best = float('inf')
    for i in range(n):
        d = seg_dist(q, poly[i], poly[(i+1)%n])
        if d < best:
            best = d
    return best

def solve():
    data = sys.stdin.read().split()
    idx = 0
    N, Q = int(data[idx]), int(data[idx+1]); idx += 2
    poly = []
    for _ in range(N):
        x, y = int(data[idx]), int(data[idx+1]); idx += 2
        poly.append((x, y))
    results = []
    for _ in range(Q):
        qx, qy = int(data[idx]), int(data[idx+1]); idx += 2
        d = min_dist_to_convex(poly, (qx, qy))
        if d == 0.0:
            results.append("INSIDE 0.000000")
        else:
            results.append(f"OUTSIDE {d:.6f}")
    print('\n'.join(results))

solve()

Step-by-Step 解説

Step 1: 外積による位置関係

外積 $(A-O) \times (B-O)$ の符号: 正→$B$ は $OA$ の左(反時計方向)、負→右(時計方向)、0→共線。

Step 2: 凸多角形の内外判定 $O(\log N)$

凸多角形を頂点 $P_0$ から「扇形」に分割。クエリ点 $q$ が:

  1. 扇形の角度範囲内か: $P_0P_1$ より反時計側かつ $P_0P_{N-1}$ より時計側
  2. 二分探索でどのセクター(三角形 $P_0P_iP_{i+1}$)に属するか特定
  3. 対応する辺 $P_iP_{i+1}$ の左側にあるかチェック

Step 3: 最近点距離の計算

点 $p$ と線分 $ab$ の最近点: $t = \text{clamp}(\frac{\vec{ap} \cdot \vec{ab}}{|\vec{ab}|^2}, 0, 1)$ として $\text{最近点} = a + t \cdot \vec{ab}$。全 $N$ 辺の最小値を取る。

よくあるミス

ミス原因正しい書き方
扇形外の点をチェックしない角度範囲チェックが抜けるc1 < 0 or cn > 0 で早期リターン
外積の符号を間違える時計/反時計の方向性混乱反時計回りの多角形では「左側=正」
線分距離で t のクランプを忘れる直線との距離になるt = max(0, min(1, ...))
整数演算のオーバーフロー$10^9$ 座標の外積Python は多倍長なので問題なし

次のステップ

  • 発展: 凸多角形と凸多角形の距離計算(回転キャリパー法 $O(N+M)$)
  • 発展: 動的凸包(Li Chao Tree による双対変換)
  • 発展: 凸多角形のミンコフスキー和

自己評価

理解度:

自分の回答:

気づき・メモ: