問題
$N$ 頂点の多角形 $P_1,\dots,P_N$(この順に辺で結ばれ $P_N$ と $P_1$ も結ばれる。凸とは限らない。辺同士は端点以外で交わらないが、向きは時計回り・反時計回りのどちらでもよい)と $Q$ 個の点が与えられる。各点について内部なら IN、辺上なら ON、外部なら OUT を出力せよ。
入力形式
N
x_1 y_1
...
x_N y_N
Q
px_1 py_1
...
px_Q py_Q
制約
| 変数 | 範囲 | 補足 |
|---|---|---|
| $N$ | $3\le N\le2000$ | 単純多角形(自己交差なし) |
| $Q$ | $1\le Q\le2000$ | $O(NQ)=4\times10^6$ で素朴解が通る |
| 座標 | $|x|,|y|\le10^9$ の整数 | 外積は $10^{18}$ 規模(Python は多倍長で安全) |
入出力例
入力例1
8
0 0
6 0
6 2
2 2
2 4
6 4
6 6
0 6
7
1 3
4 3
3 1
6 3
2 3
7 7
3 5出力例1
IN
OUT
IN
OUT
ON
OUT
IN「C」の字型。$(4,3)$ は凹んだ内側で外部、$(2,3)$ は凹みの縦辺上、$(6,3)$ は開口部で外部。
概念図:C 字型多角形と 7 つのクエリ点(実測結果)
ヒント(段階的開示)
ヒント1(方向性)
凸多角形なら「すべての辺について点が左側か」で判定できた(Day 136)が、非凸では使えない。代わりに「点から見て境界を一周したとき、境界が点の周りを何回まわるか」(回転数)を考える。外部の点では正味 0 回、内部では $\pm1$ 回。
ヒント2(アプローチ)
回転数は角度を積分しなくても、各辺が「点を通る水平線」を上向きに横切るか下向きに横切るかを数えるだけで求まる。$a_y\le p_y<b_y$(上向き)かつ点が辺の左側なら +1、$b_y\le p_y<a_y$(下向き)かつ点が右側なら −1。左右は外積 $\mathrm{cross}(b-a,p-a)$ の符号。整数座標なら浮動小数点は一切不要。
ヒント3(誘導)
wn = 0
for i in range(n):
a, b = poly[i], poly[(i+1) % n]
if on_segment(p, a, b): return "ON"
if a[1] <= p[1]:
if b[1] > p[1] and cross(a, b, p) > 0: wn += 1 # 上向き横断・点は左
else:
if b[1] <= p[1] and cross(a, b, p) < 0: wn -= 1 # 下向き横断・点は右
return "IN" if wn != 0 else "OUT"
模範解答 (Python)
import sys
def cross(ox, oy, ax, ay, bx, by):
return (ax - ox) * (by - oy) - (ay - oy) * (bx - ox)
def on_segment(px, py, ax, ay, bx, by):
# 共線 かつ 座標範囲内(整数演算のみ)
return (cross(ax, ay, bx, by, px, py) == 0
and min(ax, bx) <= px <= max(ax, bx)
and min(ay, by) <= py <= max(ay, by))
def classify(poly, px, py):
"""回転数(winding number)で IN / ON / OUT を返す。向きは問わない"""
n = len(poly)
wn = 0
for i in range(n):
ax, ay = poly[i]
bx, by = poly[(i + 1) % n]
if on_segment(px, py, ax, ay, bx, by):
return "ON"
if ay <= py:
# 辺が上向きに水平線 y=py を横切り、点が辺の左側なら +1
if by > py and cross(ax, ay, bx, by, px, py) > 0:
wn += 1
else:
# 辺が下向きに横切り、点が辺の右側なら -1
if by <= py and cross(ax, ay, bx, by, px, py) < 0:
wn -= 1
return "IN" if wn != 0 else "OUT"
def main():
data = sys.stdin.buffer.read().split()
idx = 0
n = int(data[idx]); idx += 1
poly = []
for _ in range(n):
poly.append((int(data[idx]), int(data[idx + 1]))); idx += 2
q = int(data[idx]); idx += 1
out = []
for _ in range(q):
px, py = int(data[idx]), int(data[idx + 1]); idx += 2
out.append(classify(poly, px, py))
print("\n".join(out))
main()
Step-by-Step 解説
1半開区間 $a_y\le p_y<b_y$ が頂点を正しく扱う
点と同じ高さに頂点があるとき、頂点を共有する 2 辺の両方を数えると二重カウント。「下端は含み上端は含まない」半開区間にすると頂点は上側の辺だけで 1 回数えられる(極大・極小なら 0 回か 2 回で打ち消す)。
点と同じ高さに頂点があるとき、頂点を共有する 2 辺の両方を数えると二重カウント。「下端は含み上端は含まない」半開区間にすると頂点は上側の辺だけで 1 回数えられる(極大・極小なら 0 回か 2 回で打ち消す)。
2実測(C 字型・本ページ作成時に Python で実行)
$-1\le x,y\le7$ の格子点を乱択 5000 回試して交差数法と全件一致(不一致 0)。逆順(時計回り)にすると (1,3) の $wn=-1$ だが判定は wn != 0 なので IN のまま——向きを揃える前処理が不要。
| 点 | $wn$ | 判定 | 交差数(even–odd)法 |
|---|---|---|---|
| (1,3) | 1 | IN | IN |
| (4,3) | 0 | OUT(凹みの中) | OUT |
| (3,1) | 1 | IN | IN |
| (6,3) | 0 | OUT(開口部) | OUT |
| (2,3) | — | ON | ON |
| (7,7) | 0 | OUT | OUT |
| (3,5) | 1 | IN | IN |
3両手法が「違う答え」を出す場合(自己交差多角形)
五芒星(半径 100 の円周上を丸めた $(0,100),(-59,-81),(95,31),(-95,31),(59,-81)$)の中心 $(0,0)$ では
本問の制約(辺は端点以外で交わらない)を外すと規則の選択で答えが変わる。SVG の fill-rule="nonzero" / "evenodd" がこの 2 つに対応する。
五芒星(半径 100 の円周上を丸めた $(0,100),(-59,-81),(95,31),(-95,31),(59,-81)$)の中心 $(0,0)$ では
| 手法 | 中心 (0,0) | 先端付近 (0,50) |
|---|---|---|
| 回転数法(nonzero) | $wn=2$ → IN | $wn=1$ → IN |
| 交差数法(even–odd) | 交差 2 回 → OUT | IN |
迷った他案と採らなかった理由
| 案 | 採否 | 理由 |
|---|---|---|
| 角度の総和(atan2 の差分を足して $2\pi$ で割る) | 却下 | 浮動小数点の累積誤差で $wn\approx0.9999$ となり閾値判定が必要。$10^9$ 座標では危険 |
| 交差数(even–odd)法 | 同等・不採用 | 単純多角形では等価で実装量も同じ。Step 3 の差異を示すため回転数法を主に据えた |
| 凸分割してから凸判定 | 却下 | 前処理 $O(N\log N)$〜$O(N^2)$ で各判定 $O(\log N)$ になるが、$N,Q\le2000$ では $O(NQ)$ で十分。$Q\ge10^6$ なら判断が変わる(スラブ分割など) |
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| $a_y\le p_y\le b_y$ と閉区間にして頂点を二重カウント | 2 辺の両方が条件を満たす | 下端含む・上端含まない半開区間 |
| 外積の符号だけで左右を決め、辺の向きを見ない | 下向き辺では左右が逆転 | 上向きは cross > 0 で +1、下向きは cross < 0 で −1 |
| ON 判定を後回しにする | 辺上の点は横断判定が不定 | 各辺で最初に on_segment を確認し即 ON |
| 浮動小数点で交点の $x$ を計算 | $10^9$ 座標同士の除算で誤差 | 外積の符号比較だけで済ませる |
次のステップ
- 関連する実問題: AOJ CGL_3_C「Polygon–Point Containment」(同一の出力仕様)、ABC 296 G「Polygon and Points」(凸版・二分探索が必要)
- 発展: 穴のある多角形——回転数は閉路ごとの和で自然に拡張でき、穴の向きを外周と逆にする GIS の慣行と nonzero 規則の関係を確認する
- 扱わなかった範囲: $Q\sim10^6$ の前処理型(スラブ・台形分割)、自己交差多角形でどちらの規則を採るかの仕様判断
- 次回予告: 次セッションで別テーマへ