問題
$N$ 頂点の凸多角形(反時計回りに与えられる)と $Q$ 個の点クエリ $q_i = (x_i, y_i)$ が与えられる。
各クエリに対して、以下を答えよ:
- 内外判定: 点 $q_i$ が凸多角形の内部(辺上を含む)にあるか外部にあるか
- 最近点距離: 点 $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)$
ヒント
ヒント1(方向性)
凸多角形の内外判定は $O(\log N)$ で行える。頂点 0 を基準に「扇形分割」し、クエリ点がどの扇形セクターにあるかを二分探索で特定する。
ヒント2(アプローチ)
内外判定:
- 頂点 $P_0$ を起点とし、クエリ点 $q$ の相対ベクトルを作る
- $q$ が $\vec{P_0P_1}$ と $\vec{P_0P_{N-1}}$ の間の角度範囲内かチェック
- 二分探索でセクター特定 → セクター内の辺との外積で最終判定
最近点距離:
外部なら全 $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$ が:
- 扇形の角度範囲内か: $P_0P_1$ より反時計側かつ $P_0P_{N-1}$ より時計側
- 二分探索でどのセクター(三角形 $P_0P_iP_{i+1}$)に属するか特定
- 対応する辺 $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 による双対変換)
- 発展: 凸多角形のミンコフスキー和
自己評価
理解度:
自分の回答:
気づき・メモ: