Day 056-Q1 — Convex Hull + Rotating Calipers 最大三角形面積

2026-06-09 赤色 Master / Phase 8+ ★★★★★★★★★ 凸包 / 回転キャリパー / 幾何

問題

$N$ 個の点 $P_1, \ldots, P_N$(整数座標)が与えられる。$Q$ 個の更新クエリに答えながら、常に全点から3点を選んで作られる三角形の面積の最大値の2倍(整数)を出力せよ。

  • クエリ A: 現時点の全点で最大三角形面積×2を出力
  • クエリ U i x y: 点 $P_i$ を $(x, y)$ に更新

制約

パラメータ範囲
$N$$3 \le N \le 2000$
$Q$$1 \le Q \le 2000$
$|x_i|, |y_i|$$\le 10^9$
クエリ A の個数$\ge 1$

入出力例

入力例 1

5 3
0 0
4 0
4 4
0 4
2 6
A
U 5 2 -2
A

出力例 1

32
16

初期5点の凸包で最大三角形は頂点 $(0,0),(4,0),(2,6)$ など → 面積×2 = 32。
$P_5$ を $(2,-2)$ に更新後 → 最大三角形面積×2 = 16。

概念図: 凸包と回転キャリパー

Andrew's Monotone Chain + Rotating Calipers 凸包構築 (0,0) (4,0) (4,4) (0,4) (2,6) (2,2) 内部 最大三角形 回転キャリパー法 辺(i,j)を固定しポインタkを回転 i j k (最遠点) 最大距離 cross(hull[i], hull[j], hull[k]) = 辺(i→j)からkへの符号付き面積×2

ヒント(段階的開示)

ヒント1: 方向性
最大面積三角形の3頂点は必ず凸包の頂点から選ばれる。まず Andrew's Monotone Chain で凸包を $O(N \log N)$ で求め、その後凸包頂点 $H$ 個に対して回転キャリパー法で $O(H^2)$ で探索する。
ヒント2: アプローチ
  • 外側ループ: 頂点 $i$ を固定
  • 内側ループ: $j$ を $i+1$ から回し、$k$ をポインタで進める(リセットしない)
  • cross(hull[i], hull[j], hull[(k+1)%h]) > cross(hull[i], hull[j], hull[k%h]) の間 $k$ を進める
  • $j$ の内側ループで $k$ をリセットしないことが $O(H^2)$ の鍵
ヒント3: コード骨格
def cross(o, a, b):
    return (a[0]-o[0])*(b[1]-o[1]) - (a[1]-o[1])*(b[0]-o[0])

def max_triangle_area2(hull):
    h = len(hull)
    ans = 0
    for i in range(h - 1):
        k = i + 2
        for j in range(i + 1, h):
            while cross(hull[i], hull[j], hull[(k+1)%h]) > \
                  cross(hull[i], hull[j], hull[k%h]):
                k += 1
            ans = max(ans, cross(hull[i], hull[j], hull[k%h]))
    return ans

模範解答 (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 convex_hull(pts):
    pts = sorted(set(pts))
    n = len(pts)
    if n <= 2:
        return pts
    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)
    return lower[:-1] + upper[:-1]

def max_triangle_area2(hull):
    h = len(hull)
    if h < 3:
        return 0
    ans = 0
    for i in range(h - 1):
        k = i + 2
        for j in range(i + 1, h):
            nk = (k + 1) % h
            while cross(hull[i], hull[j], hull[nk]) > cross(hull[i], hull[j], hull[k % h]):
                k += 1
                nk = (k + 1) % h
            c = cross(hull[i], hull[j], hull[k % h])
            if c > ans:
                ans = c
    return ans

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

    out = []
    for _ in range(Q):
        line = input().split()
        if line[0] == 'A':
            hull = convex_hull(pts)
            out.append(str(max_triangle_area2(hull)))
        else:
            i, x, y = int(line[1])-1, int(line[2]), int(line[3])
            pts[i] = (x, y)

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

solve()

Step-by-Step 解説

Step 1: Andrew's Monotone Chain

座標でソートし、下側凸包・上側凸包を順に構築する。外積が非正なら後退(共線点を除外)。計算量 $O(N \log N)$。

Step 2: 最大三角形の性質

最大面積三角形の3頂点は必ず凸多角形の頂点。内部点を加えても面積は改善しない(三角形面積の単調性)。

Step 3: 回転キャリパー

辺 $(i, j)$ を固定したとき、最遠点 $k$ は $j$ を増やすにつれて単調に進む。$k$ のポインタをリセットしないため、固定 $i$ に対する全 $j$ の走査で $k$ の総進み量は $O(H)$。全体 $O(H^2)$。

計算量

処理計算量
凸包構築$O(N \log N)$
最大三角形(回転キャリパー)$O(H^2) \le O(N^2)$
クエリ A 1回$O(N \log N + N^2)$
全体(Q クエリ)$O(Q \cdot N^2)$

よくあるミス

ミス原因正しい書き方
k のリセットj ループ内で k = i+2 するとO(H³)ki 固定時に一度だけ初期化
共線点の扱いcross <= 0 で除外すると問題設定によってズレ問題に応じて < 0<= 0 を選択
整数オーバーフロー$10^9 \times 10^9$Python は任意精度なので無問題

次のステップ

  • 発展: 最大内接三角形(円周上の点での面積最大化)
  • 関連問題: AtCoder ABC 151 D, ABC 166 E(凸包応用)
  • 最遠点対(直径): 回転キャリパーで $O(H)$

自己評価

自分の回答:

気づき・メモ: