Day 021-Q5 — 凸多角形の最遠点対・回転キャリパー法

2026-05-04 赤色 Master / Phase 8+ ★★★★★★★★★ 計算幾何高度応用・回転キャリパー

問題

$N$ 点の点集合 $P = \{p_1, p_2, \ldots, p_N\}$ が与えられる。

  1. 凸包を求めよ
  2. 凸包上の最遠点対(Diameter)の距離の 2 乗を求めよ
  3. 凸包上の任意の点(整数または有理数座標)で実現される最遠点対を 1 組出力せよ

さらに、凸包の幅(Width = 最小外接帯幅)も求めよ。

入力形式

N
x_1 y_1
x_2 y_2
...
x_N y_N

制約

$3 \leq N \leq 10^5$
$-10^9 \leq x_i, y_i \leq 10^9$
整数座標
3点以上が凸包上に乗る(一般位置保証なし)

入出力例

入力例 1

5
0 0
4 0
4 3
2 5
0 3

出力例 1

34
0 0
4 3
Width: ...

ヒント (段階的開示)

ヒント1: 方向性
回転キャリパー法(Rotating Calipers): 凸包の対蹠点対を $O(N)$ で列挙するアルゴリズム。2本の「キャリパー」(平行支持線)を凸包に当てて反時計回りに回転させる。
ヒント2: アプローチ
(1) 凸包を反時計回りに構築(Graham scan / Andrew's monotone chain)、(2) 最初のキャリパー: 最も下の点と最も上の点にセット、(3) 一方のキャリパーを次の辺まで回転させ、都度距離を計算、(4) 一周したら完了。
ヒント3: 誘導
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]

模範解答 (Python)

import sys
from math import gcd

def solve():
    data = sys.stdin.read().split()
    pos = 0
    N = int(data[pos]); pos += 1
    points = []
    for _ in range(N):
        x = int(data[pos]); pos += 1
        y = int(data[pos]); pos += 1
        points.append((x, y))

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

    def dist2(A, B):
        return (A[0]-B[0])**2 + (A[1]-B[1])**2

    # Andrew's Monotone Chain 凸包
    pts = sorted(set(points))
    n = len(pts)
    if n == 1:
        print(0)
        print(pts[0][0], pts[0][1])
        print(pts[0][0], pts[0][1])
        return

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

    hull = lower[:-1] + upper[:-1]
    h = len(hull)

    if h == 1:
        print(0)
        print(hull[0][0], hull[0][1])
        print(hull[0][0], hull[0][1])
        return
    if h == 2:
        d2 = dist2(hull[0], hull[1])
        print(d2)
        print(hull[0][0], hull[0][1])
        print(hull[1][0], hull[1][1])
        return

    # 回転キャリパー法でDiameter
    max_d2 = 0
    best_pair = (hull[0], hull[1])
    j = 0
    for i in range(h):
        while True:
            nj = (j + 1) % h
            A = hull[i]
            B = hull[(i+1) % h]
            c_cur = cross(A, B, hull[j])
            c_nxt = cross(A, B, hull[nj])
            if c_nxt > c_cur:
                j = nj
            else:
                break

        for di in [hull[i], hull[(i+1)%h]]:
            d = dist2(di, hull[j])
            if d > max_d2:
                max_d2 = d
                best_pair = (di, hull[j])

    print(max_d2)
    print(best_pair[0][0], best_pair[0][1])
    print(best_pair[1][0], best_pair[1][1])

    # 幅(Width): 最小外接帯
    min_width_num = float('inf')
    min_width_den = 1

    j = 0
    for i in range(h):
        A = hull[i]
        B = hull[(i+1) % h]
        edge_len2 = dist2(A, B)
        if edge_len2 == 0:
            continue

        while True:
            nj = (j + 1) % h
            c_cur = cross(A, B, hull[j])
            c_nxt = cross(A, B, hull[nj])
            if c_nxt > c_cur:
                j = nj
            else:
                break

        c = cross(A, B, hull[j])  # = |AB| * height
        if c * c * min_width_den < min_width_num * edge_len2:
            min_width_num = c * c
            min_width_den = edge_len2

    g = gcd(min_width_num, min_width_den)
    print(f"Width^2 = {min_width_num//g}/{min_width_den//g}")

solve()

Step-by-Step 解説

1凸包構築(Andrew's Monotone Chain)
点をx座標(同じならy座標)でソートし、下側凸包と上側凸包を別々に構築。外積が $\leq 0$ の点をスタックから除く(左折りしない)。計算量: $O(N \log N)$。
2回転キャリパー法のアイデア
凸包上の対蹠点対は「回転する平行線(キャリパー)」で捉えられる。辺 $(hull[i], hull[i+1])$ を一方のキャリパーに固定し、もう一方のキャリパーが当たる最遠点 $hull[j]$ を管理。辺を一つ進めるごとに $j$ も単調に進む → 全体 $O(N)$。
3幅(Width)の計算
各辺に対する「反対側の最遠点からの距離」が外接帯幅の候補。外積 $= |辺| \times 高さ$ を利用して整数演算のまま比較。
4整数演算の精度
浮動小数点を使わず、距離の 2 乗や外積(整数)で比較することで精度を保つ。

よくあるミス

ミス原因正しい書き方
凸包を時計回りで構築cross <= 0 の条件が逆反時計回り: cross <= 0 でpop
キャリパーの進め方を誤る距離ではなく外積で進めるcross(A, B, hull[nj]) > cross(A, B, hull[j])
重複点でdivide by zeroedge_len2 が 0if edge_len2 == 0: continue

次のステップ

  • 発展問題: 最小外接円(Welzl のアルゴリズム)
  • 応用: 動的凸包(Kinetic Convex Hull)

自己評価

自分の回答

気づき・メモ