Day 010-Q2 — 高度幾何学(凸包・半平面交差)

2026-04-23 黄色 / Phase 6 ★★★★★★ 凸包・幾何

問題

平面上に $N$ 点が与えられる。これらの点を囲む凸包の周長を求めよ。

制約

$3 \le N \le 10^5$
$-10^9 \le x_i, y_i \le 10^9$
すべての点が一直線上に乗らない
同じ座標の点はない

入出力例

入力例 1

5
0 0
4 0
4 3
0 4
2 2

出力例 1

14.472135954999579

ヒント (段階的開示)

ヒント1: 方向性
Andrew's Monotone Chain で $O(N \log N)$。下側凸包と上側凸包を別々に構築して結合。
ヒント2: アプローチ
外積の符号で左折り/右折りを判定。右折りになる点はスタックから除去。
ヒント3: 誘導
while len(lower) >= 2 and cross(lower[-2], lower[-1], p) <= 0:
    lower.pop()
lower.append(p)

模範解答 (Python)

import math

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

def convex_hull(points):
    points = sorted(set(points))
    n = len(points)
    if n <= 1:
        return points

    lower = []
    for p in points:
        while len(lower) >= 2 and cross(lower[-2], lower[-1], p) <= 0:
            lower.pop()
        lower.append(p)

    upper = []
    for p in reversed(points):
        while len(upper) >= 2 and cross(upper[-2], upper[-1], p) <= 0:
            upper.pop()
        upper.append(p)

    return lower[:-1] + upper[:-1]

def dist(p1, p2):
    return math.sqrt((p1[0]-p2[0])**2 + (p1[1]-p2[1])**2)

def solve():
    N = int(input())
    points = []
    for _ in range(N):
        x, y = map(int, input().split())
        points.append((x, y))

    hull = convex_hull(points)
    m = len(hull)
    perimeter = 0.0
    for i in range(m):
        perimeter += dist(hull[i], hull[(i+1) % m])

    print(perimeter)

solve()

Step-by-Step 解説

1外積による回転方向判定
正: 反時計回り、0: 直線上、負: 時計回り。凸包は常に左折り。
2下側凸包の構築
左端から右端へスキャン。右折りまたは直線になる点を pop。
3上側凸包
右端から左端へ同様。lower[:-1] + upper[:-1] で重複端点を除く。
4周長計算
隣接頂点間のユークリッド距離の合計。

よくあるミス

ミス原因正しい書き方
cross > 0 で pop境界点が残る<= 0 で pop
上下接合で重複lower[-1] == upper[0]lower[:-1] + upper[:-1]
同一点を含むcross が 0 で無限ループset(points)

次のステップ

  • 発展問題: 凸包の面積(外積の符号付き和 / 2)
  • 応用: 半平面交差

自己評価

自分の回答

気づき・メモ