Day 136-Q5 — 凸多角形の点包含判定(扇形三角形分割上の二分探索)

2026-08-28 赤色 Master / Phase 8+ ★★★★★★★★★ P0からの角度単調性を利用しO(log N)で三角形を特定

問題

反時計回り(CCW)に頂点が与えられた凸多角形(頂点数 $N$)がある。$Q$ 個のクエリ点それぞれについて、その点が多角形の内部または境界上にあるかを判定せよ(境界上も内側として Yes)。

頂点 $P_0$ を固定し、$P_0$ から各頂点への方向ベクトルでできる扇形の中から、クエリ点がどの三角形 $(P_0,P_k,P_{k+1})$ に属するかを二分探索で特定し、各クエリを $O(\log N)$ で処理せよ。

入力形式

N
x_1 y_1
...
x_N y_N
Q
qx_1 qy_1
...
qx_Q qy_Q

制約

$3 \le N \le 2\times10^5$
$1 \le Q \le 2\times10^5$
座標の絶対値は $10^9$ 以下
多角形は狭義凸・CCW順

入出力例

入力例1

4
0 0
4 0
4 4
0 4
6
2 2
5 5
0 0
4 2
2 4
-1 2

出力例1

Yes
No
Yes
Yes
Yes
No

概念図

P0からの扇形分割 + 角度二分探索 P0 P1 P2 P3 P4 Q cross(P0→P_mid, P0→Q) の符号で二分探索 → 三角形(P0,P2,P3)に絞り込み → 最終判定1回

ヒント(段階的開示)

ヒント1(方向性)

毎クエリ全頂点との外積を調べれば$O(N)$で判定できるが、$Q$個で合計$O(NQ)$になり間に合わない。凸多角形の頂点が角度順に並んでいることを使えば二分探索で絞り込めるはずである。

ヒント2(アプローチ)

$P_0$から他の頂点への方向ベクトルは角度順に単調に並んでいる。クエリ点への方向ベクトルがどの2つの隣接頂点方向の「間」にあるかを外積の符号だけで二分探索できる。範囲が絞れたら最後にその三角形の内部判定を1回行う。

ヒント3(誘導)
def contains(pt):
    if cross(P0, poly[1], pt) < 0 or cross(P0, poly[-1], pt) > 0:
        return False  # 扇形の外側
    lo, hi = 1, N - 1
    while hi - lo > 1:
        mid = (lo + hi) // 2
        if cross(P0, poly[mid], pt) >= 0:
            lo = mid
        else:
            hi = mid
    return cross(poly[lo], poly[hi], pt) >= 0

境界(頂点上・辺上)を Yes とするため、すべての比較を等号込み(>=0 / <=0)で統一する。

模範解答 (Python)

import sys


def main():
    data = sys.stdin.buffer.read().split()
    idx = 0
    N = int(data[idx]); idx += 1
    poly = []
    for _ in range(N):
        x = int(data[idx]); idx += 1
        y = int(data[idx]); idx += 1
        poly.append((x, y))
    Q = int(data[idx]); idx += 1
    queries = []
    for _ in range(Q):
        x = int(data[idx]); idx += 1
        y = int(data[idx]); idx += 1
        queries.append((x, y))

    def cross(ax, ay, bx, by):
        return ax * by - ay * bx

    p0x, p0y = poly[0]

    def vec(px, py, qx, qy):
        return qx - px, qy - py

    def contains(px, py):
        if (px, py) == (p0x, p0y):
            return True
        v1x, v1y = vec(p0x, p0y, poly[1][0], poly[1][1])
        vpx, vpy = vec(p0x, p0y, px, py)
        if cross(v1x, v1y, vpx, vpy) < 0:
            return False
        vnx, vny = vec(p0x, p0y, poly[N - 1][0], poly[N - 1][1])
        if cross(vnx, vny, vpx, vpy) > 0:
            return False

        lo, hi = 1, N - 1
        while hi - lo > 1:
            mid = (lo + hi) // 2
            vmx, vmy = vec(p0x, p0y, poly[mid][0], poly[mid][1])
            if cross(vmx, vmy, vpx, vpy) >= 0:
                lo = mid
            else:
                hi = mid

        ax, ay = poly[lo]
        bx, by = poly[hi]
        ex, ey = vec(ax, ay, bx, by)
        fx, fy = vec(ax, ay, px, py)
        return cross(ex, ey, fx, fy) >= 0

    out = []
    for (px, py) in queries:
        out.append("Yes" if contains(px, py) else "No")

    print("\n".join(out))


main()

Step-by-Step 解説

1扇形の外側を高速に除外
$P_0\to P_1$と$P_0\to P_{N-1}$の外側にクエリ点があれば、2回の外積計算だけで多角形の外だと即座に判定できる。
2角度順の単調性で二分探索
$P_0$から各頂点への方向ベクトルは反時計回りに単調に回転しているため、cross(P0→P_mid, P0→Q)の符号だけで二分探索が成立する。
3最終確認は三角形1つだけ
絞り込んだ範囲$[lo,hi]$($hi=lo+1$)の三角形について、辺$P_{lo}\to P_{hi}$に対する外積の符号で内外が確定する。
4境界条件を等号込みで統一
頂点上・辺上のケースをすべてYesとするため、扇形判定・二分探索・最終判定のすべてを>=0 / <=0で統一する。
5計算量
前処理不要で各クエリは外積計算$O(\log N)$回のみなので、全体で$O(Q\log N)$になる。

よくあるミス

ミス原因正しい書き方
$P_0$自身をクエリすると壊れる方向ベクトルが零ベクトルになり外積が常に0最初にpt==P0を特別扱いしてTrueを返す
二分探索の境界条件で無限ループlo<hiで書きmidの選び方を誤るhi-lo>1を条件にし2頂点を確定させる
不等号の等号を落とす境界上の点をすべてNoと誤判定すべての比較で等号を含める
多角形が時計回り(CW)で誤判定外積の符号の意味が反転する反時計回りを前提にするか、事前に向きを確認・反転する
座標をfloatに変換してしまう誤差で境界判定がぶれるPythonのintは多倍長なので整数演算のまま扱う

次のステップ

  • 発展: 頂点の追加・削除がオンラインで発生する動的凸包でも同様の判定ができるよう、平衡二分探索木で頂点列を管理する設計を考えよ。
  • 発展2: 「点から凸多角形への接線を求める」問題に拡張し、同じ二分探索の考え方が使えるか検証せよ。

自己評価

自分の回答

気づき・メモ