Day 029-Q2 — 多次元凸包・半空間交差・最遠点クエリ

2026-05-12 赤色 Master / Phase 8+ ★★★★★★★★★ 凸包・三分探索

問題

2次元平面上に $N$ 点が与えられる。HULL(凸包の頂点列挙)と FARTHEST u($P_u$ から最遠の凸包点)クエリに答えよ。

制約

$3 \le N \le 3 \times 10^5$
$1 \le Q \le 10^5$
$|x|, |y| \le 10^9$ 整数
HULL 最大10回 / FARTHEST 最大 $10^5$ 回

入出力例

入力例 1

5
0 0
4 0
4 3
0 3
2 1
5
HULL
FARTHEST 5
FARTHEST 1
FARTHEST 3
HULL

出力例 1

1 2 3 4
3
3
1
1 2 3 4

ヒント (段階的開示)

ヒント1: 方向性
Andrew's Monotone Chain で凸包構築 $O(N \log N)$、三分探索で最遠点 $O(\log N)$。
ヒント2: アプローチ
凸包は X 座標ソート後に下側・上側を構築。任意点から凸包頂点への距離は単峰なので三分探索可。
ヒント3: 同距離処理
三分探索後は候補範囲を全チェックして最小インデックスを選択。距離は二乗距離(オーバーフロー回避)。

模範解答 (Python)

import sys
input = sys.stdin.readline

def cross(O, A, B):
    return (A[0]-O[0])*(B[1]-O[1]) - (A[1]-O[1])*(B[0]-O[0])

def dist2(p, q):
    return (p[0]-q[0])**2 + (p[1]-q[1])**2

def build_hull(pts_idx):
    sorted_pts = sorted(pts_idx, key=lambda t: (t[1][0], t[1][1]))
    lower, upper = [], []
    for p in sorted_pts:
        while len(lower) >= 2 and cross(lower[-2][1], lower[-1][1], p[1]) <= 0:
            lower.pop()
        lower.append(p)
    for p in reversed(sorted_pts):
        while len(upper) >= 2 and cross(upper[-2][1], upper[-1][1], p[1]) <= 0:
            upper.pop()
        upper.append(p)
    return lower[:-1] + upper[:-1]

def farthest_on_hull(hull, p):
    n = len(hull)
    if n == 1:
        return hull[0][0] + 1
    lo, hi = 0, n - 1
    for _ in range(200):
        if lo >= hi - 1:
            break
        m1 = lo + (hi - lo) // 3
        m2 = hi - (hi - lo) // 3
        if dist2(hull[m1][1], p) < dist2(hull[m2][1], p):
            lo = m1 + 1
        else:
            hi = m2 - 1
    best_d, best_i = -1, float('inf')
    for i in range(max(0, lo-1), min(n, hi+2)):
        d = dist2(hull[i][1], p)
        idx1 = hull[i][0] + 1
        if d > best_d or (d == best_d and idx1 < best_i):
            best_d, best_i = d, idx1
    return best_i

def solve():
    N = int(input())
    pts = []
    for _ in range(N):
        x, y = map(int, input().split())
        pts.append((x, y))
    pts_idx = [(i, pts[i]) for i in range(N)]
    hull = build_hull(pts_idx)
    Q = int(input())
    out = []
    for _ in range(Q):
        line = input().split()
        if line[0] == 'HULL':
            out.append(' '.join(str(p[0]+1) for p in hull))
        else:
            u = int(line[1]) - 1
            out.append(str(farthest_on_hull(hull, pts[u])))
    print('\n'.join(out))

solve()

Step-by-Step 解説

1Andrew's Monotone Chain
X 座標ソート後、下側・上側凸包を別々に構築。cross が 0 以下で中間点を削除。
2三分探索で最遠点
凸包上で距離は単峰。十分な反復で最大点を $O(\log N)$ 特定。
3同距離処理
候補範囲を全チェック、最小インデックスを選ぶ。
4クエリ処理
HULL は凸包を 1-indexed で出力、FARTHEST は三分探索。

よくあるミス

ミス原因正しい書き方
cross の符号共線点を含むかで使い分け問題に応じて <= 0 / < 0
三分探索の収束不足反復回数が少ない50〜100回
1/0-indexed 混在入出力変換ミス入力直後に統一

次のステップ

  • 動的点追加に対するオンライン凸包 + 最遠点 $O(\log^2 N)$

自己評価

自分の回答

気づき・メモ