코스

기하 알고리즘

CCW·볼록껍질·스위핑·고급 계산기하.

Level 4 → Level 9 23 아이템 8 문제 15 강의 0 확인 문제
코스 진행도 0%
0 / 23 아이템 완료
01
Level 4 · Challenger

Challenger

기하 알고리즘 · Challenger 단계

0/10 완료
Lesson CCW와 외적 — 방향·넓이·교차의 원리 필수 8m 현재

어떤 문제를 푸는가

평면 위 세 점 \(A, B, C\)가 주어졌을 때 이들이 반시계(CCW)·시계(CW)·일직선(collinear)
어느 배치인지, 나아가 두 선분이 교차하는지 를 판정합니다. CCW(counter-clockwise) 판정은
계산 기하의 가장 기본 원자 연산으로, 볼록 껍질·다각형 포함 판정·선분 교차·각도 정렬 등
사실상 모든 기하 알고리즘이 이 하나 위에 세워집니다.

  • 입력이 좌표(점·선분) 이고 "방향", "교차", "왼쪽/오른쪽", "내부/외부"를 물으면 CCW를 떠올립니다.
  • 핵심 도구는 외적(cross product) 한 번. 나눗셈·기울기·삼각함수 없이 부호만으로 판정합니다.

CCW — 부호 있는 외적

벡터 \(\vec{AB}=(B-A)\)\(\vec{AC}=(C-A)\)의 2차원 외적을 정의합니다.

$$ \operatorname{ccw}(A,B,C)=(B_x-A_x)(C_y-A_y)-(B_y-A_y)(C_x-A_x) $$

부호 기하적 의미
\(>0\) \(A\to B\to C\)반시계(좌회전), \(C\)\(\vec{AB}\)왼쪽
\(<0\) 시계(우회전), \(C\)\(\vec{AB}\)오른쪽
\(=0\) 세 점이 한 직선 위(공선)

이 값의 절댓값 \(|\operatorname{ccw}(A,B,C)|\)은 삼각형 \(ABC\) 넓이의 2배 입니다. 즉 하나의 정수
연산이 방향(부호)넓이(크기) 를 동시에 담습니다.


왜 부호가 방향을 결정하는가

외적은 두 벡터가 만드는 평행사변형의 부호 있는 넓이 입니다. 오른손 좌표계에서 \(\vec{AC}\)
\(\vec{AB}\)를 기준으로 왼쪽으로 벌어지면 양의 넓이, 오른쪽이면 음의 넓이가 됩니다. "회전 방향"이
곧 "왼쪽에 있는가 오른쪽에 있는가"이므로, 외적 부호 하나로 회전 방향이 완전히 결정됩니다.

기울기 \(\frac{C_y-A_y}{C_x-A_x}\)를 비교하는 방식은 수직선에서 분모가 0이 되고 부동소수 오차가
끼지만, 외적은 곱셈·뺄셈뿐이라 정수 입력이면 완전히 정확 합니다.


선분 교차의 원리

선분 \(\overline{AB}\)\(\overline{CD}\)가 만나려면 두 조건이 동시에 성립해야 합니다.

  1. \(C\)\(D\)가 직선 \(AB\)서로 반대편:
    \(\operatorname{ccw}(A,B,C)\)\(\operatorname{ccw}(A,B,D)\)의 부호가 다르다.
  2. \(A\)\(B\)가 직선 \(CD\)서로 반대편:
    \(\operatorname{ccw}(C,D,A)\)\(\operatorname{ccw}(C,D,B)\)의 부호가 다르다.

$$ \operatorname{ccw}(A,B,C)\cdot\operatorname{ccw}(A,B,D)<0 \;\land\; \operatorname{ccw}(C,D,A)\cdot\operatorname{ccw}(C,D,B)<0 $$

두 조건이 모두 참이면 두 선분은 내부에서 가로질러 만납니다.

예시. \(A(0,0),B(4,4),C(0,4),D(4,0)\)이면 두 대각선이 \((2,2)\)에서 교차합니다.
\(\operatorname{ccw}(A,B,C)=16>0\), \(\operatorname{ccw}(A,B,D)=-16<0\)로 부호가 엇갈리고,
반대쪽도 마찬가지라 교차가 확인됩니다.


까다로운 경계 — 끝점 닿음과 공선

위 곱이 \(0\)이 되는 순간(어떤 \(\operatorname{ccw}=0\), 즉 세 점 공선)은 별도 처리해야 합니다.

  • 끝점이 다른 선분 위에 닿는 경우(예: \(C\)\(\overline{AB}\) 내부에 위치).
  • 두 선분이 같은 직선 위 에서 일부 구간을 겹치는 경우.

이 경우는 부호만으로는 알 수 없고, 좌표 구간이 겹치는지(1차원 겹침)를 추가로 봐야 합니다.
이 경계 처리를 빠뜨리는 것이 이 단원 오답의 단골 원인입니다. 구체적 판정식은 2강에서 다룹니다.


복잡도와 핵심 직관

CCW 한 번은 \(O(1)\), 선분 교차 판정도 \(O(1)\)입니다. 복잡한 기하 판정이 결국 외적 부호 몇 개의
조합
으로 환원된다는 점, 그리고 정수로 계산하면 오차가 아예 없다는 점이 이 단원의 두 가지 핵심
교훈입니다. 다음 강의에서 오버플로 없이 안전하게 구현합니다.

Lesson 정수 CCW와 선분 교차 구현 선택 8m

정수로 안전한 CCW (C++)

좌표가 정수면 외적도 정수이므로 부동소수 오차가 원천적으로 0 입니다. 값 대신 부호만
돌려주는 습관을 들이면 이후 비교에서 다시 곱하다 넘치는 사고를 막습니다.

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

struct P { ll x, y; };

// +1: 반시계, -1: 시계, 0: 일직선
int ccw(const P& a, const P& b, const P& c) {
    ll v = (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
    return (v > 0) - (v < 0);
}

오버플로 경계. 좌표가 최대 \(10^9\)이면 차 \((b.x-a.x)\)는 최대 \(2\times10^9\), 곱은
\(4\times10^{18}\)까지 커집니다. long long의 한계 \(\approx9.2\times10^{18}\) 안이라 아슬아슬하게
버티지만, 좌표가 \(10^9\)넘거나 중간에 좌표를 더하면 넘칩니다. 그때는 __int128을 씁니다.

int ccw128(const P& a, const P& b, const P& c) {
    __int128 v = (__int128)(b.x - a.x) * (c.y - a.y)
               - (__int128)(b.y - a.y) * (c.x - a.x);
    return (v > 0) - (v < 0);
}

선분 교차 — 공선까지 완전 처리 (C++)

// 공선일 때 c가 선분 [a,b] 위에 있는가 (바운딩 박스 검사)
bool onSeg(const P& a, const P& b, const P& c) {
    return min(a.x, b.x) <= c.x && c.x <= max(a.x, b.x)
        && min(a.y, b.y) <= c.y && c.y <= max(a.y, b.y);
}
bool operator<=(const P& a, const P& b) {           // 사전순 (정규화용)
    return a.x != b.x ? a.x < b.x : a.y <= b.y;
}

bool intersect(P a, P b, P c, P d) {
    int ab = ccw(a, b, c) * ccw(a, b, d);
    int cd = ccw(c, d, a) * ccw(c, d, b);
    if (ab == 0 && cd == 0) {           // 네 점 공선 → 구간 겹침으로 판정
        if (b <= a) swap(a, b);
        if (d <= c) swap(c, d);
        return c <= b && a <= d;        // 정렬 후 겹침 조건
    }
    return ab <= 0 && cd <= 0;          // 끝점 닿음(=0)도 교차로 포함
}
  • ab <= 0 && cd <= 0 — 끝점이 다른 선분에 닿는 경우까지 교차 로 봅니다(약한 교차).
    끝점 닿음을 교차로 치지 않으려면(강한/진짜 교차) 두 곱 모두 < 0으로 바꿉니다.
  • 공선 케이스는 두 선분을 각각 사전순으로 정규화한 뒤 1차원 구간 \([a,b]\)\([c,d]\)의 겹침
    (\(c\le b\) 그리고 \(a\le d\), 여기서 비교는 사전순)으로 판정합니다.

검증 예시. 겹치는 공선: \(A(0,0)B(4,0)\), \(C(2,0)D(6,0)\) → 정규화 후 \(c(2,0)\le b(4,0)\) 그리고
\(a(0,0)\le d(6,0)\) 참 → 교차. 떨어진 공선: \(C(5,0)D(6,0)\)\(c(5,0)\le b(4,0)\) 거짓 → 비교차.
한 점 접촉: \(C(4,0)D(6,0)\)\(c(4,0)\le b(4,0)\) 참, \(a(0,0)\le d(6,0)\) 참 → 교차(끝점 닿음).


파이썬 구현

파이썬 정수는 자동 다정밀도라 오버플로 걱정이 없습니다.

def ccw(a, b, c):
    v = (b[0]-a[0])*(c[1]-a[1]) - (b[1]-a[1])*(c[0]-a[0])
    return (v > 0) - (v < 0)

def on_seg(a, b, c):
    return min(a[0],b[0]) <= c[0] <= max(a[0],b[0]) and \
           min(a[1],b[1]) <= c[1] <= max(a[1],b[1])

def intersect(a, b, c, d):
    ab = ccw(a,b,c) * ccw(a,b,d)
    cd = ccw(c,d,a) * ccw(c,d,b)
    if ab == 0 and cd == 0:                 # 공선: 구간 겹침
        a, b = sorted((a, b)); c, d = sorted((c, d))
        return c <= b and a <= d
    return ab <= 0 and cd <= 0

흔한 함정

  • 오버플로 — 가장 자주 틀립니다. 좌표가 크면 long long, 넘으면 __int128. 부호만 반환.
  • 공선(=0) 누락 — 곱의 부호만 보면 \(0\)을 놓쳐 끝점 닿음·구간 겹침을 오판합니다.
  • 부동소수 도피 — 기울기·교점 좌표를 실수로 구하면 수직선과 오차에 당합니다. 정수는 끝까지 정수로.
  • 끝점 포함 여부 — "닿음이 교차인가"를 문제에서 확인하고 <=/<를 결정.
  • 퇴화 선분(길이 0) — 같은 두 점으로 된 입력은 점-선분 포함으로 별도 처리.
Lesson 심화·응용 — 넓이·내부 판정·각도 정렬과 함정 선택 8m

출제 신호 — 언제 이 도구를 꺼내는가

  • 입력이 좌표(점·선분·다각형)이고 방향(시계/반시계/일직선), 선분 교차, 점의 다각형 내부 여부를 묻는 문제.
  • "두 길/울타리가 만나는가", "겹치는가" 같은 서사로 변장한 교차 판정.
  • 좌표 범위가 \(|x|,|y|\le10^9\)처럼 큰 정수 — 실수 기하가 아니라 정수 CCW 로 풀라는 신호.
  • 볼록 껍질·다각형 넓이·각도 정렬 등 상위 기하 알고리즘의 부품 으로 항상 등장.

응용 1 — 다각형 넓이 (신발끈 공식)

CCW의 크기가 삼각형 넓이의 2배라는 사실에서 곧바로 나옵니다. 원점을 기준으로 한 삼각형들의
부호 있는 넓이를 모두 더하면, 오목·볼록에 관계없이 단순 다각형의 넓이가 됩니다.

$$ \text{Area}=\frac12\left|\sum_{i=0}^{n-1}(x_i y_{i+1}-x_{i+1}y_i)\right| $$

ll area2(const vector<P>& p) {          // 넓이의 2배 (정수로 정확)
    ll s = 0; int n = p.size();
    for (int i = 0; i < n; i++) {
        const P& a = p[i]; const P& b = p[(i + 1) % n];
        s += a.x * b.y - b.x * a.y;      // = ccw(원점,a,b)
    }
    return llabs(s);                     // 실제 넓이는 s/2, 홀수면 .5
}

넓이의 2배 를 정수로 유지하면 반정수 넓이까지 오차 없이 다룰 수 있습니다.


응용 2 — 점의 다각형 내부 판정

볼록 다각형 이면 모든 변에 대해 CCW 부호가 일관되면 내부입니다(\(O(n)\); 정점 각도 이분탐색으로
\(O(\log n)\)까지 가능). 일반(오목) 다각형 이면 점에서 오른쪽으로 쏜 반직선이 변과 몇 번
교차하는지 세는 홀짝 규칙(ray casting) 을 씁니다 — 홀수면 내부.

// 일반 다각형: 광선 교차 홀짝. 경계 위 점은 별도 처리 필요.
bool inPolygon(const vector<P>& poly, P q) {
    int n = poly.size(), cnt = 0;
    for (int i = 0; i < n; i++) {
        P a = poly[i], b = poly[(i + 1) % n];
        bool cond = (a.y > q.y) != (b.y > q.y);      // y구간이 q를 감싸는가
        if (cond) {
            // 교점의 x가 q.x보다 오른쪽인가 (외적으로 나눗셈 없이)
            ll lhs = (b.x - a.x) * (q.y - a.y);
            ll rhs = (q.x - a.x) * (b.y - a.y);
            if (a.y < b.y ? lhs > rhs : lhs < rhs) cnt ^= 1;
        }
    }
    return cnt & 1;
}

응용 3 — 각도 정렬 (나눗셈 없는 극각 비교)

기준점을 중심으로 점들을 반시계 순으로 정렬할 때, \(\operatorname{atan2}\)의 부동소수 대신 CCW를
비교자로 씁니다. 먼저 위/아래 반평면 으로 나눈 뒤 같은 반평면 안에서 CCW로 좌우를 정합니다.

int half(const P& p) { return (p.y != 0) ? (p.y > 0 ? 1 : -1)
                                         : (p.x >= 0 ? 1 : -1); }
bool angleCmp(const P& a, const P& b) {          // 원점 기준 극각 오름차순
    int ha = half(a), hb = half(b);
    if (ha != hb) return ha < hb;
    ll cr = a.x * b.y - a.y * b.x;               // = ccw(원점,a,b)
    return cr > 0;                               // 같으면 정렬 무관(공선)
}

함정 총정리와 변형

  • 오버플로가 1순위. 넓이 합·각도 비교에서도 곱이 커지므로 long long/__int128을 유지.
  • 경계 위 점. 내부 판정에서 "경계도 내부로 보는가"를 문제에서 확인 — ray casting은 경계에서
    불안정하므로, 경계 포함이 중요하면 각 변에 대해 onSeg를 먼저 검사.
  • 공선/퇴화. 넓이 0(모두 공선), 길이 0 선분, 자기 교차 다각형은 별도 가정 확인.
  • 강/약 교차. 끝점 닿음 포함 여부에 따라 <=<. 문제 정의 한 줄이 정답을 가릅니다.
  • 변형: 다각형-다각형 교차, 볼록 판정, 반직선/직선 교차 모두 위 CCW 부품의 조합으로 확장됩니다.

태그된 연습 문제를 CCW 단독 → 접촉 불포함 교차 → 접촉·공선 포함 교차 → 넓이/내부 판정 순으로
풀며, 반례(겹치는 구간·한 점 접촉·떨어진 공선)를 직접 만들어 자기 코드로 검증하는 습관이 가장
효과적입니다.

Practice problem 직각이등변삼각형 선택 25m
KOI00002

직각이등변삼각형

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
Practice problem 울타리 넓이 선택 25m
R00236

울타리 넓이

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
Lesson 볼록 껍질의 개념과 모노톤 체인 원리 필수 8m

어떤 문제를 푸는가

평면 위 \(N\)개의 점이 주어졌을 때, 그 모든 점을 포함하는 가장 작은 볼록 다각형
(볼록 껍질, convex hull)을 구합니다. 점들에 고무줄을 씌웠을 때 만들어지는 바깥 윤곽이라고
생각하면 됩니다. 지름(가장 먼 두 점), 최소 외접 도형, 폭, 충돌 영역, 컨벡스 헐 트릭 등
수많은 기하·최적화 문제의 기반 구조 입니다.

  • 입력이 점 집합 이고 "외곽선", "가장 바깥", "감싸는", "가장 먼 두 점"을 물으면 볼록 껍질입니다.
  • 도구는 CCW(외적) 하나. 정렬 후 스택으로 \(O(N\log N)\)에 정확히 구합니다.

볼록이란 무엇인가

다각형이 볼록 하다는 것은 경계를 따라 한 바퀴 돌 때 항상 같은 방향(예: 계속 좌회전)으로만
꺾인다는 뜻입니다. 어디선가 반대로 꺾이면(우회전) 오목한 부분이 생긴 것입니다. 이 "회전 방향"을
바로 CCW(외적 부호)로 판정하므로, 볼록 껍질은 CCW 위에 세워집니다.

동치 정의: 껍질 위 임의의 두 점을 이은 선분이 항상 다각형 내부에 놓입니다.


모노톤 체인의 아이디어 (Andrew's monotone chain)

대표 알고리즘은 그라함 스캔과 모노톤 체인 두 가지지만 원리는 같습니다: 점을 정렬한 뒤 스택에
쌓아가며 볼록을 깨는 점을 제거
합니다. 모노톤 체인은 정렬 기준이 단순(좌표 사전순)해 구현이
깔끔하고 실수하기 어려워 실전에서 선호됩니다.

  1. 점들을 \(x\) 좌표(같으면 \(y\)) 기준으로 사전순 정렬 한다.
  2. 왼쪽→오른쪽으로 훑으며 아래 껍질(lower hull) 을 만든다. 새 점을 넣을 때 직전 두 점과 함께
    보아 좌회전이 아니면(우회전 또는 일직선) 직전 점을 스택에서 빼낸다.
  3. 오른쪽→왼쪽으로 다시 훑으며 같은 규칙으로 위 껍질(upper hull) 을 만든다.
  4. 둘을 이으면 전체 볼록 껍질(반시계 방향).

예시.\((0,0),(1,1),(2,0),(1,-1),(1,0)\)에서 가운데 \((1,0)\)과 정렬 시 내부에 들어가는
\((1,1)\)·\((1,-1)\)의 처리 결과, 껍질은 바깥 네 점 \((0,0),(1,-1),(2,0),(1,1)\)이 됩니다(공선 점 \((1,0)\)은 제거).


왜 옳은가

볼록 껍질의 경계는 정렬 순서로 보면 "한 방향으로만 꺾이는" 점들의 수열입니다. 스택에 점을 쌓다가
직전 세 점이 잘못된 회전(오목을 만드는 방향)을 이루면, 가운데 점은 껍질에 속할 수 없으므로 제거
합니다. 아래 껍질은 아래쪽 경계를, 위 껍질은 위쪽 경계를 정확히 재구성합니다. 각 점은 스택에 한 번
들어가고 최대 한 번 빠지므로 스캔은 선형입니다.


복잡도

단계 시간
정렬 \(O(N\log N)\)
스캔(각 점 1회 push, 최대 1회 pop) \(O(N)\)
전체 \(O(N\log N)\)

정렬이 비용을 지배합니다. 좌표가 정수면 CCW를 정수로 계산해 오차가 전혀 없습니다.


경계 처리 미리보기 — 일직선 위의 점

세 점이 공선(CCW \(=0\))일 때 가운데 점을 껍질에 포함할지 는 문제 정의에 따라 다릅니다. 보통은
제거(엄격한 볼록 껍질)하지만, "경계 위 점도 껍질에 센다"는 문제면 포함해야 합니다. 이 한 끗 차이가
정답을 가르므로 조건을 꼭 확인하세요. 다음 강의에서 정수 안전 구현과 이 분기 처리를 봅니다.

Lesson 정수 모노톤 체인 구현과 공선 처리 선택 8m

모노톤 체인 구현 (C++, 정수)

CCW는 외적으로 계산해 부동소수 오차를 없앱니다.

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

struct P { ll x, y; };
bool operator<(const P& a, const P& b) {
    return a.x != b.x ? a.x < b.x : a.y < b.y;
}
bool operator==(const P& a, const P& b) { return a.x == b.x && a.y == b.y; }

// 양수: 반시계(좌회전)
ll cross(const P& O, const P& A, const P& B) {
    return (A.x - O.x) * (B.y - O.y) - (A.y - O.y) * (B.x - O.x);
}

vector<P> convexHull(vector<P> p) {
    sort(p.begin(), p.end());
    p.erase(unique(p.begin(), p.end()), p.end());   // 중복 점 제거
    int n = p.size(), k = 0;
    if (n <= 2) return p;
    vector<P> h(2 * n);
    for (int i = 0; i < n; i++) {                    // 아래 껍질
        while (k >= 2 && cross(h[k-2], h[k-1], p[i]) <= 0) k--;
        h[k++] = p[i];
    }
    for (int i = n - 2, t = k + 1; i >= 0; i--) {    // 위 껍질
        while (k >= t && cross(h[k-2], h[k-1], p[i]) <= 0) k--;
        h[k++] = p[i];
    }
    h.resize(k - 1);          // 마지막은 시작점과 중복 → 제거
    return h;                 // 반시계 방향, 시작점 1회만 포함
}

오버플로. 좌표가 최대 \(10^9\)이면 cross의 곱이 약 \(4\times10^{18}\)까지 커집니다. 반드시
long long(넘으면 __int128)을 씁니다. 기하의 단골 함정입니다.


공선 처리 — <= 0< 0

핵심 한 줄은 스택 pop 조건입니다.

  • cross(...) <= 0 — 일직선(공선) 점을 제거. 껍질 변 위의 중간 점이 결과에서 빠집니다.
    (엄격한 볼록 껍질)
  • cross(...) < 0 — 공선 점을 유지. "경계 위의 모든 점을 껍질에 포함"하라는 문제에 씁니다.

문제 조건("껍질 위 점의 개수를 세라" 등)에 따라 이 부등호 하나로 정답이 갈립니다. < 0으로 바꾸면
한 변에 여러 공선 점이 남으므로, 결과에서 중복·역방향 정리를 신경 써야 합니다.


파이썬 구현

def convex_hull(pts):
    pts = sorted(set(pts))
    if len(pts) <= 2:
        return pts

    def cross(o, a, b):
        return (a[0]-o[0])*(b[1]-o[1]) - (a[1]-o[1])*(b[0]-o[0])

    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]        # 끝점 중복 제거

그라함 스캔 변형 (극각 정렬)

가장 아래-왼쪽 점을 피벗으로 잡고 나머지를 극각(반시계) 순으로 정렬한 뒤, 스택으로 우회전을
제거합니다. 결과는 같지만 정렬 비교자로 CCW를 써야 하고 동일 각도·공선 처리가 까다로워, 실전에서는
모노톤 체인을 더 선호합니다.

// 피벗(pivot) 기준 극각 비교: 나눗셈 없이 CCW로
P pivot;
bool polarCmp(const P& a, const P& b) {
    ll c = cross(pivot, a, b);
    if (c != 0) return c > 0;                        // 반시계 먼저
    // 같은 각도면 피벗에서 가까운 점 먼저 (공선 안정화)
    ll da = (a.x-pivot.x)*(a.x-pivot.x) + (a.y-pivot.y)*(a.y-pivot.y);
    ll db = (b.x-pivot.x)*(b.x-pivot.x) + (b.y-pivot.y)*(b.y-pivot.y);
    return da < db;
}

흔한 함정

  • 오버플로crosslong long/__int128. 가장 자주 틀립니다.
  • 공선 부등호<= 0(제거) vs < 0(포함)을 문제 정의에 맞춰 선택.
  • 중복·동일 점sort + unique(파이썬 set)로 먼저 정리.
  • 점이 2개 이하 — 다각형이 안 되므로 별도 반환.
  • 모든 점이 공선 — 껍질이 선분으로 퇴화. 상위 알고리즘이 이를 가정하지 않으면 예외 처리.
  • 끝점 중복 — 아래·위 껍질을 이을 때 시작·끝점이 겹치므로 하나씩 잘라냅니다.
Lesson 심화·응용 — 지름·내부 판정·헐 트릭 선택 8m

출제 신호

  • 점 집합에서 "가장 바깥 윤곽", "감싸는 최소 볼록 다각형", "껍질 위 점의 개수".
  • 껍질을 부품 으로 쓰는 문제: 가장 먼 두 점(지름), 최소 외접 사각형/원, 폭, 두 볼록 도형의 최소
    거리 — 모두 껍질을 먼저 구한 뒤 회전하는 캘리퍼스(상위 단원)로 잇습니다.
  • 컨벡스 헐 트릭 처럼 "직선들의 하한 껍질"로 DP를 가속하는 최적화형.

응용 1 — 넓이·둘레와 지름

껍질을 구하면 신발끈 공식으로 넓이(2배를 정수로), 변 길이 합으로 둘레를 즉시 얻습니다. 가장 먼 두
점(지름)은 껍질 위에서만 나타나므로, 껍질 정점 수를 \(h\)라 하면 회전하는 캘리퍼스로 \(O(h)\)
찾습니다(다음 단원). 무작정 \(O(N^2)\)로 모든 쌍을 보는 대신 껍질로 후보를 줄이는 것이 정석입니다.

ll area2(const vector<P>& h) {          // 껍질 넓이의 2배
    ll s = 0; int n = h.size();
    for (int i = 0; i < n; i++)
        s += h[i].x * h[(i+1)%n].y - h[(i+1)%n].x * h[i].y;
    return llabs(s);
}

응용 2 — 볼록 다각형 안의 점 판정 \(O(\log N)\)

껍질이 반시계로 정렬돼 있으면, 첫 정점을 기준으로 부채꼴을 이분탐색해 점이 어느 삼각형에
속하는지 찾고 CCW 한 번으로 내부를 확정합니다.

// h: 반시계 볼록 다각형. 내부(경계 포함)면 true.
bool inConvex(const vector<P>& h, P q) {
    int n = h.size();
    if (cross(h[0], h[1], q) < 0 || cross(h[0], h[n-1], q) > 0) return false;
    int lo = 1, hi = n - 1;
    while (hi - lo > 1) {                 // q가 속한 부채꼴 이분탐색
        int mid = (lo + hi) / 2;
        (cross(h[0], h[mid], q) >= 0 ? lo : hi) = mid;
    }
    return cross(h[lo], h[hi], q) >= 0;   // 마지막 변 안쪽인가
}

응용 3 — 컨벡스 헐 트릭(개념)

DP 전이 \(dp[i]=\min_j(a_j\cdot x_i+b_j)\) 형태는 직선 \(y=a_j\ x+b_j\)들의 하한 껍질(lower envelope)
위에서 최소를 찾는 문제로 바뀝니다. 기울기 순으로 직선을 추가하며 필요 없는 직선을 pop하는 구조가
바로 볼록 껍질의 1차원판입니다. 쿼리·기울기가 단조면 \(O(N)\), 아니면 Li Chao 트리로 \(O(N\log)\).
여기서도 교점 비교를 정수 외적 으로 처리하면 오차가 없습니다.


함정과 변형 총정리

  • 오버플로가 여전히 1순위. 넓이·지름·헐 트릭 교점 모두 곱이 커지므로 long long/__int128.
  • 공선 정책 일관성. 지름·최소 외접처럼 껍질을 부품으로 쓸 때는 보통 공선 점을 제거(엄격)하는
    편이 후속 캘리퍼스가 깔끔합니다. 반대로 "경계 점 개수"를 물으면 포함.
  • 퇴화 입력. 점 1~2개, 모든 점 공선, 모든 점 동일 — 상위 알고리즘 호출 전에 걸러냅니다.
  • 동적/온라인 볼록 껍질. 점이 삽입·삭제되며 껍질을 유지해야 하면 균형 BST 기반의 동적 껍질
    (\(O(\log N)\) 삽입)로 확장합니다 — 상위 주제.
  • 3D 볼록 껍질. 평면을 넘어가면 증분법·선물 포장 등 별도 기법이 필요합니다(범위 밖).

볼록 껍질은 "회전 방향이 한쪽으로만 유지된다"는 성질 위에서 정렬 + 스택으로 \(O(N\log N)\)
구해집니다. CCW를 정수로 안전하게 다루는 습관이 이 위에 얹히는 모든 기하 응용의 안정성을 좌우합니다.

Practice problem 가장 먼 두 관측 지점 선택 25m
R00259

가장 먼 두 관측 지점

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
Practice problem 천문대 관측 영역 선택 25m
R00258

천문대 관측 영역

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
02
Level 6 · Strategist

Strategist

기하 알고리즘 · Strategist 단계

0/5 완료
Lesson 회전하는 캘리퍼스 — 안티포달 쌍과 단조성 필수 8m

어떤 문제를 푸는가

볼록 다각형(보통 볼록 껍질)이 주어졌을 때, 그 위를 두 개의 평행한 지지선(caliper) 이 감싸며
회전한다고 상상합니다. 껍질을 한 바퀴 도는 동안 두 접점이 단조롭게 함께 나아가는 성질을 이용해,
\(O(N^2)\)가 걸릴 법한 최적화들을 껍질 위에서 \(O(N)\) 에 해결하는 기법이 회전하는 캘리퍼스
(rotating calipers)입니다.

풀 수 있는 대표 문제:

  • 지름 — 가장 먼 두 점 사이 거리.
  • 폭(width) — 다각형을 감싸는 평행선 사이 최소 간격.
  • 최소 외접 직사각형 — 넓이/둘레가 최소인 감싸는 사각형.
  • 두 볼록 다각형 사이 최소/최대 거리.
  • 가장 먼 두 점의 쌍이 이루는 최대 삼각형 등.

전제는 항상 하나: 대상이 볼록 하고 정점이 회전 순(반시계) 으로 정렬돼 있어야 합니다.


안티포달 쌍과 단조성

두 점이 다각형을 감싸는 평행한 지지선 위에 동시에 놓일 수 있으면 안티포달(antipodal) 쌍
이라 합니다. 가장 먼 두 점은 반드시 안티포달 쌍이며, 볼록 껍질의 정점 에서만 나타납니다.

핵심 성질(단조성): 한 지지선이 껍질을 따라 반시계로 조금 회전하면, 맞은편 접점도 뒤로 가지 않고
같은 방향으로 전진합니다. 따라서 한 점 \(i\)를 앞으로 옮길 때 대응점 \(j\)도 앞으로만 옮기면 되고,
\(i\)\(j\)가 각각 껍질을 한 바퀴 도는 동안 총 이동은 \(O(N)\)입니다. 이 "두 포인터가 되돌아오지
않는다"는 것이 캘리퍼스의 전부입니다.


접점의 전진을 무엇으로 판정하는가 — 외적

"\(j\)를 한 칸 더 전진시켜야 하는가"는 두 변의 외적(면적) 비교 로 정합니다. 변
\(\overline{p_i p_{i+1}}\)을 밑변으로 볼 때, 삼각형 \(p_i\ p_{i+1} p_j\)의 넓이가 \(j{+}1\)에서 더
커지면(즉 \(p_j\)가 아직 가장 먼 정점이 아니면) \(j\)를 전진시킵니다.

$$ \text{cross}(p_{i+1}-p_i,\; p_{j+1}-p_i) > \text{cross}(p_{i+1}-p_i,\; p_{j}-p_i) $$

거리(제곱)를 직접 비교하지 않고 외적 넓이 로 전진을 판정하므로, 나눗셈·제곱근 없이 정수만으로
정확하게 돌아갑니다.


복잡도와 전제 조건

단계 시간
볼록 껍질 구성 \(O(N\log N)\)
캘리퍼스 한 바퀴 \(O(H)\) (\(H\)=껍질 정점 수)

지배 항은 껍질을 만드는 정렬입니다. 캘리퍼스 자체는 선형. 반드시 껍질을 먼저 구하고, 정점이
반시계로 정렬됐으며 공선 점을 제거(엄격 껍질)한 상태에서 시작해야 안전합니다. 다음 강의에서
지름을 정수로 구현하고, 이어 폭·최소 외접 사각형으로 확장합니다.

Lesson 지름 구현과 다각형 간 거리 선택 8m

지름 (가장 먼 두 점) — C++ 정수 구현

껍질 h는 앞 단원의 convexHull로 얻은 반시계·공선 제거 상태라고 가정합니다. 거리 제곱과
전진 판정 모두 정수 외적으로 처리합니다.

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

struct P { ll x, y; };
ll cross(P O, P A, P B) {                // (A-O) x (B-O)
    return (A.x-O.x)*(B.y-O.y) - (A.y-O.y)*(B.x-O.x);
}
ll dist2(P a, P b) {                      // 거리의 제곱 (정수)
    ll dx = a.x - b.x, dy = a.y - b.y;
    return dx*dx + dy*dy;
}

// 볼록 껍질 h(반시계, 공선 제거) 위에서 지름^2 과 두 점의 인덱스
ll diameter2(const vector<P>& h, int& bi, int& bj) {
    int n = h.size();
    if (n == 1) { bi = bj = 0; return 0; }
    if (n == 2) { bi = 0; bj = 1; return dist2(h[0], h[1]); }
    ll best = 0; int j = 1;
    for (int i = 0; i < n; i++) {
        int ni = (i + 1) % n;
        // 밑변 h[i]->h[ni] 에 대해 가장 먼 정점까지 j를 전진
        while (cross(h[i], h[ni], h[(j+1)%n]) > cross(h[i], h[ni], h[j]))
            j = (j + 1) % n;
        // 밑변 양 끝과 대응점 사이 거리 후보 갱신
        for (int e : {i, ni}) {
            ll d = dist2(h[e], h[j]);
            if (d > best) { best = d; bi = e; bj = j; }
        }
    }
    return best;
}
  • cross 비교로 전진 을 판정하므로 제곱근·나눗셈이 없습니다.
  • 밑변의 양 끝점 \(h[i], h[ni]\)을 모두 후보로 넣는 것이 흔한 실수 방지 포인트입니다. 한쪽만
    보면 특정 안티포달 쌍을 놓칩니다.
  • 실제 지름은 \(\sqrt{\text{best}}\)지만, 문제에서 제곱 비교 만 요구하면 정수 best를 그대로
    씁니다(오차 0).

예시. 껍질 \((0,0),(4,0),(4,3),(0,3)\)의 지름은 대각선 \((0,0)\)\((4,3)\)으로 \(d^2=25\), 즉 \(5\)입니다.


두 볼록 다각형 사이 최소 거리

두 볼록 다각형 \(P,Q\) 사이 최소 거리는 각각 아래/위 지지선을 맞물려 캘리퍼스로 훑으며, 점-선분
거리
후보를 갱신해 \(O(|P|+|Q|)\)에 구합니다. 정수 좌표라면 거리 제곱과 사영 판정을 유리수(분자·분모
정수)로 다뤄 오차를 피할 수 있습니다.

// 점 p 와 선분 ab 사이 거리^2 (부동소수). 대량 비교엔 유리수화 권장.
double segDist2(P a, P b, P p) {
    ll dx = b.x-a.x, dy = b.y-a.y;
    ll ap_ab = (p.x-a.x)*dx + (p.y-a.y)*dy;
    ll ab2 = dx*dx + dy*dy;
    double t = ab2 ? (double)ap_ab / ab2 : 0.0;
    t = max(0.0, min(1.0, t));            // 선분 밖이면 끝점으로 클램프
    double cx = a.x + t*dx, cy = a.y + t*dy;
    double ex = p.x - cx, ey = p.y - cy;
    return ex*ex + ey*ey;
}

흔한 함정

  • 껍질 전제 위반 — 입력이 볼록이 아니거나 공선 점이 남아 있으면 단조성이 깨져 오답. 반드시
    엄격 껍질 을 먼저 만듭니다.
  • 밑변 한쪽만 후보 — 위 코드처럼 \(i\)\(ni\) 양 끝을 모두 비교해야 모든 안티포달 쌍을 봅니다.
  • \(N\le2\) 퇴화 — 점 1~2개는 별도 분기. 모든 점 공선이면 껍질이 선분이 되므로 지름은 양 끝 거리.
  • 제곱근 남용 — 거리 크기 비교엔 \(d^2\)(정수)만 쓰고, 실제 길이가 필요할 때만 마지막에 한 번
    \(\sqrt{}\). 중간 비교에 실수를 끼우지 마세요.
  • 오버플로\(d^2\)는 좌표 \(10^9\)에서 \(\approx8\times10^{18}\)까지 갑니다. long long 경계선,
    넘으면 __int128.
Lesson 심화·응용 — 폭·최소 외접 사각형·변형 선택 8m

응용 1 — 다각형의 폭(최소 지지선 간격)

폭은 다각형을 감싸는 평행선 사이의 최소 간격입니다. 반드시 어느 한 지지선이 껍질의 한 변과
겹치므로, 각 변을 밑변으로 잡고 그 변에서 가장 먼 정점까지의 수직 거리 를 캘리퍼스로 훑어
최소를 취합니다. 수직 거리 \(=\dfrac{|\text{cross}(\vec{e},\vec{ep})|}{|\vec e|}\)이므로, 비교는
\(\dfrac{\text{cross}^2}{|\vec e|^2}\)(정수 분자/분모)로 하여 제곱근을 마지막까지 미룹니다.

// 최소 폭^2 을 유리수 비교로. width2 = cross^2 / edgeLen^2 의 최소.
// 반환은 (분자, 분모) 형태로 두어 오차 없는 비교가 가능.
pair<ll,ll> minWidth2(const vector<P>& h) {
    int n = h.size(); int j = 1;
    ll bn = -1, bd = 1;                          // best = bn/bd
    for (int i = 0; i < n; i++) {
        int ni = (i + 1) % n;
        while (llabs(cross(h[i], h[ni], h[(j+1)%n]))
             > llabs(cross(h[i], h[ni], h[j]))) j = (j + 1) % n;
        ll c = cross(h[i], h[ni], h[j]);         // 밑변에서 j까지 (넓이 2배)
        ll el = dist2(h[i], h[ni]);              // 밑변 길이^2
        // 수직거리^2 = c^2 / el.  bn/bd 와 c*c / el 비교
        if (bn < 0 || (__int128)c*c*bd < (__int128)bn*el) { bn = c*c; bd = el; }
    }
    return {bn, bd};
}

폭은 최소 외접 평행사변형·충돌 판정(분리축) 등에 그대로 쓰입니다.


응용 2 — 최소 외접 직사각형

넓이가 최소인 감싸는 직사각형은 반드시 한 변이 껍질의 한 변과 평행 합니다(회전 정리). 각 변을
기준 방향으로 삼아, 그 방향으로 가장 먼 정점(높이), 좌·우로 가장 먼 정점(폭)을 세 개의 캘리퍼스
로 동시에 전진시키며 넓이를 갱신합니다. 전체 \(O(H)\).

  • 높이 접점: 밑변에서 외적 넓이가 최대인 정점(위 폭 코드와 동일한 전진).
  • 좌/우 접점: 밑변 방향 벡터와의 내적(dot) 이 각각 최소·최대인 정점.

내적·외적 모두 정수이므로 넓이 비교(\(=\) 폭×높이, 유리수)를 오차 없이 처리할 수 있습니다. 둘레
최소 사각형도 같은 골격에서 목적 함수만 바꿉니다.

ll dot(P O, P A, P B) {                          // (A-O)·(B-O)
    return (A.x-O.x)*(B.x-O.x) + (A.y-O.y)*(B.y-O.y);
}

응용 3 — 두 다각형의 최대 거리 / 최대 삼각형

  • 두 볼록 다각형의 가장 먼 두 점 도 두 다각형에 각각 캘리퍼스를 물려 \(O(|P|+|Q|)\)에 구합니다.
  • 껍질 위 세 점으로 만드는 최대 넓이 삼각형 은 두 정점을 캘리퍼스로 전진시키며 세 번째 정점의
    외적 넓이를 최대화합니다(주의: 순수 이중 캘리퍼스는 반례가 있어, 안전하게는 각 밑변마다 대응점을
    단조 전진시키는 신중한 구현 또는 \(O(H^2)\) 보강이 필요).

함정과 변형 총정리

  • 볼록·반시계·공선 제거 전제 를 항상 먼저 보장. 이것이 캘리퍼스 오답의 최대 원인입니다.
  • 제곱근·나눗셈은 마지막에. 전진 판정과 크기 비교는 외적·내적·유리수(분자/분모)로. 실제 길이가
    필요할 때만 한 번 \(\sqrt{}\).
  • 오버플로. \(c^2\), \(c^2\cdot d\) 형태는 쉽게 \(10^{18}\)를 넘습니다. 비교식에 __int128을 쓰세요.
  • 퇴화. 껍질이 점·선분으로 줄어드는 입력(\(N\le2\), 전부 공선)은 별도 분기.
  • 변형. 폭·최소 외접 사각형·두 다각형 거리·분리축 충돌 판정은 모두 "단조 전진하는 접점"이라는
    같은 골격에서 목적 함수만 바꿔 얻습니다. 골격을 한 번 체화하면 넓은 문제군이 열립니다.
Practice problem 가장 먼 관측소 선택 25m
R00412

가장 먼 관측소

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
Practice problem 최소 넓이 외접 직사각형 선택 25m
R00657

최소 넓이 외접 직사각형

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
03
Level 8 · Expert

Expert

기하 알고리즘 · Expert 단계

0/4 완료
Lesson 반평면 교집합 — 볼록 영역과 각도 정렬 덱 필수 8m

어떤 문제를 푸는가

반평면(half-plane) 은 하나의 직선이 평면을 둘로 나눌 때 그 한쪽 전체를 말합니다. 여러 개의
부등식(직선)이 주어졌을 때, 그 모든 반평면의 공통 영역 — 즉 부등식들을 동시에 만족하는 점들의
집합 — 을 구하는 것이 반평면 교집합(half-plane intersection, HPI) 입니다. 각 반평면이 볼록이고
볼록 집합의 교집합도 볼록이므로, 결과는 볼록 다각형(비어 있거나, 무한히 뻗은 영역일 수도 있음)
입니다.

  • 입력이 직선/부등식들 이고 "동시에 만족하는 영역", "실현 가능 영역", "커널(kernel)"을 물으면 HPI.
  • 선형 계획법(2D LP) 의 실현 가능 영역, 다각형의 핵(kernel)(전체를 볼 수 있는 점들의 집합),
    볼록 다각형들의 교집합 등이 모두 HPI로 환원됩니다.

반평면을 방향 있는 직선으로 표현

반평면을 "점 \(p\)를 지나고 방향 \(\vec d\)로 뻗는 직선의 왼쪽"으로 통일해 표현합니다. 점 \(q\)
이 반평면 안(경계 포함)에 있으려면

$$ \vec d \times (q-p) \ge 0 \quad(\text{외적이 음수가 아니면 왼쪽}) $$

이 규약을 정하면 모든 부등식을 "왼쪽" 형태로 맞춘 뒤 같은 방식으로 처리할 수 있습니다. 각 직선의
방향각 \(\theta=\operatorname{atan2}(d_y,d_x)\)이 알고리즘의 정렬 키가 됩니다.


핵심 아이디어 — 각도 정렬 + 덱

교집합의 경계는 반평면들의 직선 조각이 방향각 순서대로 이어 붙은 볼록 다각형입니다. 그래서:

  1. 모든 반평면을 방향각으로 정렬 한다.
  2. 덱(deque) 에 직선을 하나씩 밀어 넣으며, 새 직선이 들어올 때 앞/뒤에서 이미 쓸모없어진
    직선을 제거
    한다. "직전 두 직선의 교점이 새 반평면 밖에 있으면" 그 직전 직선은 교집합 경계에
    기여할 수 없으므로 뺀다.
  3. 각도가 한 바퀴를 돌면 앞뒤를 맞물려 닫고, 남은 직선들의 교점이 곧 결과 다각형의 꼭짓점.

각 직선은 덱에 한 번 들어가고 최대 한 번 빠지므로, 정렬을 빼면 선형 입니다.


복잡도

단계 시간
방향각 정렬 \(O(N\log N)\)
덱 스위프 \(O(N)\)
전체 \(O(N\log N)\)

\(N\)은 반평면(부등식)의 수. 정렬이 비용을 지배합니다.


결과의 세 가지 형태

HPI의 답은 항상 셋 중 하나입니다. 이 분류를 코드가 구분하게 만드는 것이 정확성의 절반입니다.

  • 유계 볼록 다각형 — 정상적인 교집합(꼭짓점 \(\ge 3\)).
  • 공집합 — 부등식들이 모순(예: 서로 반대인 두 반평면). 덱에 유효 직선이 부족하거나 교점이
    뒤집힘.
  • 무계(unbounded) 영역 — 한 방향으로 무한히 열림. 실무에서는 아주 큰 경계 상자(bounding box)
    네 반평면을 미리 추가해 항상 유계로 만든 뒤 처리하는 것이 안전합니다.

예시. \(x\ge0,\;y\ge0,\;x+y\le2\) 세 반평면의 교집합은 꼭짓점 \((0,0),(2,0),(0,2)\)인 삼각형입니다.
다음 강의에서 이 알고리즘을 정확한 교점 공식과 함께 구현하고, 정밀도·퇴화 처리를 다룹니다.

Lesson 반평면 교집합 구현과 정밀도·무계 처리 선택 8m

반평면 교집합 구현 (C++)

반평면은 부동소수 교점을 다루므로 보통 double로 구현합니다. 아래는 각도 정렬 + 덱 의 표준
형태로, 앞 강의의 규약(직선의 왼쪽이 반평면)을 그대로 씁니다.

#include <bits/stdc++.h>
using namespace std;
const double EPS = 1e-9;

struct P { double x, y; };
P operator+(P a, P b){ return {a.x+b.x, a.y+b.y}; }
P operator-(P a, P b){ return {a.x-b.x, a.y-b.y}; }
P operator*(P a, double t){ return {a.x*t, a.y*t}; }
double cross(P a, P b){ return a.x*b.y - a.y*b.x; }

struct Line {
    P p, d;              // 점 p, 방향 d. 반평면 = 직선의 '왼쪽'
    double ang;
    Line(){}
    Line(P a, P b){ p = a; d = b - a; ang = atan2(d.y, d.x); }
};
// q 가 반평면 안(왼쪽)인가
bool onLeft(const Line& l, P q){ return cross(l.d, q - l.p) > EPS; }
// 두 직선의 교점 (평행이 아님을 가정)
P inter(const Line& a, const Line& b){
    double t = cross(b.d, a.p - b.p) / cross(a.d, b.d);
    return a.p + a.d * t;
}
// 방향각 정렬. 각이 같으면 더 '안쪽(왼쪽)'인 직선을 앞에 둔다.
bool cmp(const Line& a, const Line& b){
    if (fabs(a.ang - b.ang) > EPS) return a.ang < b.ang;
    return cross(b.d, a.p - b.p) > 0;      // a 가 더 제약적이면 먼저
}

// 교집합 다각형의 꼭짓점(반시계). 비었으면 빈 벡터.
vector<P> halfPlaneIntersection(vector<Line> ls){
    sort(ls.begin(), ls.end(), cmp);
    int n = ls.size();
    deque<Line> dq;                        // 유효 직선
    deque<P> pt;                           // 인접 직선의 교점 (dq보다 하나 적음)
    for (int i = 0; i < n; i++){
        if (i && fabs(ls[i].ang - ls[i-1].ang) < EPS) continue;   // 같은 각 → 앞의 것만
        while (!pt.empty() && !onLeft(ls[i], pt.back()))  { pt.pop_back();  dq.pop_back();  }
        while (!pt.empty() && !onLeft(ls[i], pt.front())) { pt.pop_front(); dq.pop_front(); }
        if (!dq.empty()) pt.push_back(inter(dq.back(), ls[i]));
        dq.push_back(ls[i]);
    }
    // 앞뒤 맞물림: 뒤쪽 직선들이 맨 앞 직선 밖의 교점을 만들면 제거
    while (!pt.empty() && !onLeft(dq.front(), pt.back())){ pt.pop_back(); dq.pop_back(); }
    if (dq.size() < 3) return {};          // 공집합 또는 무계
    pt.push_back(inter(dq.back(), dq.front()));
    return vector<P>(pt.begin(), pt.end());
}
  • onLeft엄격 부등호(> EPS) 인 점에 주의: 경계에 딱 닿는 퇴화를 안정적으로 다루려면 EPS로
    완충합니다. dq.size() < 3은 결과가 다각형을 이루지 못한 경우(공집합/무계)를 걸러냅니다.
  • 같은 방향각 직선은 더 안쪽(제약적)인 것만 남기고 나머지는 건너뜁니다(cmp의 2차 기준 +
    루프의 continue).

검증. \(x\ge0\): Line({0,0},{0,1})(위로, 왼쪽=\(x\le0\)? 규약에 맞춰 방향을 잡아야 함),
실제로는 각 부등식을 "왼쪽" 규약에 맞도록 두 점 순서를 정합니다. 정사각형 4변을 반시계 경계로 주면
결과가 그 사각형으로 나오는지로 손검증하는 것이 가장 확실합니다.


무계 방지 — 경계 상자 추가

무계 영역을 유계로 만들려면 아주 큰 상자의 네 변을 반평면으로 미리 넣습니다.

double B = 1e9;                             // 좌표 범위보다 충분히 크게
vector<Line> box = {
    Line({-B,-B},{ B,-B}),                  // 아래
    Line({ B,-B},{ B, B}),                  // 오른쪽
    Line({ B, B},{-B, B}),                  // 위
    Line({-B, B},{-B,-B}),                  // 왼쪽 (반시계, 안쪽이 왼쪽)
};

이러면 답이 항상 유계 다각형이 되어 넓이·꼭짓점 처리가 단순해집니다. 다만 상자 크기 \(B\)를 너무
크게 잡으면 교점 좌표가 커져 정밀도가 나빠지니, 좌표 범위에 맞춰 적당히 잡습니다.


흔한 함정

  • 평행선 나눗셈inter의 분모 cross(a.d,b.d)가 0(평행)이면 나눗셈 폭발. 같은 각 처리와
    덱 규칙이 이 상황을 애초에 만들지 않도록 막지만, 방향/부호 규약이 어긋나면 발생합니다.
  • 각도 규약 불일치 — 모든 부등식을 "직선의 왼쪽" 하나로 통일하지 않으면 오답. 두 점 순서로
    방향을 맞추세요.
  • EPS 선택 — 너무 크면 얇은 정답 영역을 없애고, 너무 작으면 경계 퇴화에서 흔들립니다. 좌표
    스케일에 맞춰 \(10^{-9}\sim10^{-6}\).
  • 공집합/무계 미구분dq.size() < 3과 상자 기법으로 명시적으로 처리.
Lesson 심화·응용 — 핵·2D LP·매개변수 탐색 선택 8m

출제 신호

  • 여러 선형 부등식/직선 이 주어지고 "동시에 만족하는 영역의 넓이/존재 여부"를 묻는 문제.
  • 다각형의 핵(kernel) — 다각형 내부에서 모든 점을 볼 수 있는 관측 위치들의 집합.
  • 2D 선형 계획법 — 목적 함수 최댓값이 실현 가능 영역(HPI 결과)의 꼭짓점에서 나옴.
  • 여러 볼록 다각형의 교집합(각 다각형을 변마다 반평면으로 분해).

응용 1 — 다각형의 핵 (kernel)

단순 다각형이 반시계로 주어질 때, 각 변을 "내부가 왼쪽"인 반평면으로 바꿔 모두 교집합하면 그 결과가
바로 핵입니다. 핵이 비지 않으면 그 다각형은 별 모양(star-shaped) 이며, 핵 안의 아무 점에서나
다각형 전체를 볼 수 있습니다.

// 반시계 단순 다각형 poly 의 핵을 HPI로 구성
vector<Line> kernelHalfPlanes(const vector<P>& poly){
    vector<Line> ls; int n = poly.size();
    for (int i = 0; i < n; i++)
        ls.push_back(Line(poly[i], poly[(i+1)%n]));   // 내부가 왼쪽
    return ls;                                        // halfPlaneIntersection(ls)
}

결과 다각형이 비어 있으면(\(\text{dq.size}<3\)) 핵이 없는 것이고, 넓이 \(>0\)이면 별 모양입니다.
"경비원 한 명으로 전시장 전체를 볼 수 있는가" 류의 문제가 정확히 이 형태입니다.


응용 2 — 2D 선형 계획법

목적 \(\max (c_x x + c_y y)\)를 제약 \(A_i x + B_i y \le C_i\) 아래에서 푸는 문제는, 각 제약을 반평면으로
바꿔 실현 가능 영역을 HPI로 구한 뒤 꼭짓점만 후보로 목적 함수를 평가하면 됩니다(볼록 다각형의
선형 함수 최댓값은 꼭짓점에서). 실현 가능 영역이 비면 해가 없고, 무계면 목적 함수 방향에 따라
\(+\infty\)일 수 있습니다.

참고: 무작위 증분법(randomized incremental LP)은 2D LP를 기대 \(O(N)\)에 직접 풀지만, 영역 자체가
필요하면 HPI가 자연스럽습니다.


응용 3 — "최소 반지름으로 모두 감싸기" 류 매개변수 탐색

"반지름 \(r\)의 조건을 만족하는 배치가 존재하는가?"처럼 \(r\)을 이분탐색 하고, 각 후보 \(r\)에 대해
제약들이 반평면 집합으로 표현되면 HPI가 비지 않는지로 판정 함수를 만드는 패턴이 자주 등장합니다.
이때 \(r\)이 반평면의 오프셋 을 밀어(직선을 평행 이동) 실현 가능 영역이 언제 사라지는지를 봅니다.


함정과 변형 총정리

  • 부동소수 본질. HPI는 교점이 유리수/무리수라 정수화가 어렵습니다. EPS 관리와 좌표 스케일링이
    정확성의 핵심입니다.
  • 퇴화 처리. 세 반평면 이상이 한 점에서 만나는 경우, 완전히 평행한 반평면들, 폭 0의 정답 영역
    등은 EPS와 dq.size()<3 규칙으로 걸러냅니다.
  • 각도 경계. \(\operatorname{atan2}\)\(\pm\pi\) 근처에서 감싸므로, 같은 방향각 판정과 정렬
    안정성에 주의합니다.
  • 무계 vs 공집합 구분. 답을 "유계/공집합/무계" 셋으로 명확히 분류하고, 실무에선 경계 상자로
    무계를 유계화한 뒤 넓이를 재는 것이 안전합니다.
  • 변형. 볼록 다각형 교집합, 핵, 2D LP, 매개변수 이분탐색의 판정 함수 — 모두 "부등식을 왼쪽
    반평면으로 통일 → 각도 정렬 → 덱" 이라는 하나의 골격에서 나옵니다.
Practice problem 반평면의 교집합 넓이 선택 25m
R00654

반평면의 교집합 넓이

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
04
Level 9 · Master

Master

기하 알고리즘 · Master 단계

0/4 완료
Lesson 보로노이와 델로네 — 쌍대 구조와 빈 외접원 필수 8m

어떤 문제를 푸는가

평면 위 점(사이트, site) 집합이 주어졌을 때, 각 점마다 "이 점이 가장 가까운 사이트인 영역"
으로 평면을 나눈 것이 보로노이 다이어그램(Voronoi diagram) 입니다. 각 셀은 볼록 다각형(무한히
뻗을 수 있음)이고, 셀의 경계는 두 사이트로부터 등거리 인 점들의 자취(수직이등분선 조각)입니다.

  • "가장 가까운 시설", "각 지점에서 제일 가까운 기지국", "경계까지 최단 거리" 류가 보로노이의 언어.
  • 보로노이는 최근접점 질의, 유클리드 최소 신장 트리(EMST), 최대 공터 원, 최근접 쌍 등 수많은
    근접성(proximity) 문제의 공통 뼈대입니다.

보로노이와 델로네: 쌍대 구조

보로노이 다이어그램의 쌍대(dual)델로네 삼각분할(Delaunay triangulation) 입니다.

  • 두 사이트의 보로노이 셀이 변을 공유 ⟺ 두 사이트가 델로네 삼각분할에서 간선으로 연결.
  • 보로노이 꼭짓점(세 셀이 만나는 점) ⟺ 델로네 삼각형(그 세 사이트의 외접원 중심).

실전에서는 보로노이를 직접 만드는 대신 델로네를 만들고 쌍대로 옮기는 경우가 많습니다. 델로네가
정수 술어(외접원 판정)로 다루기 쉽고, 대부분의 응용(EMST, 최근접점 등)은 델로네 간선만 있으면
충분하기 때문입니다.


델로네의 정의와 빈 외접원 성질

삼각분할이 델로네라는 것은 모든 삼각형의 외접원 내부에 다른 사이트가 없다(empty circumcircle)는
뜻입니다. 이 성질 덕분에 델로네는 "가장 뚱뚱한(최소각을 최대화하는)" 삼각분할이 되어 수치적으로
좋습니다. 핵심 술어는 inCircle: 점 \(d\)가 삼각형 \(abc\)의 외접원 내부 인지 판정.

$$ \operatorname{inCircle}(a,b,c,d)=\det\begin{pmatrix} a_x-d_x & a_y-d_y & (a_x-d_x)^2+(a_y-d_y)^2\\ b_x-d_x & b_y-d_y & (b_x-d_x)^2+(b_y-d_y)^2\\ c_x-d_x & c_y-d_y & (c_x-d_x)^2+(c_y-d_y)^2\end{pmatrix} $$

\(a,b,c\)가 반시계일 때 이 행렬식이 \(>0\)이면 \(d\)는 외접원 , \(=0\)이면 원 위, \(<0\)이면 밖입니다.
좌표가 정수면 이 판정도 정수로 정확히 됩니다(단, 값이 커서 __int128 필요).


대표 구성 알고리즘과 복잡도

방법 복잡도 특징
Fortune's 스위프라인 \(O(N\log N)\) 빗자루선 + 해변선. 구현 난도 상
분할 정복 \(O(N\log N)\) 이론적으로 우아, 병합이 까다로움
증분법(incremental) 평균 \(O(N\log N)\), 최악 \(O(N^2)\) 점을 하나씩 삽입 + 뒤집기(flip). 구현 단순

\(N\)개 사이트의 델로네/보로노이는 간선·꼭짓점 수가 모두 \(O(N)\)이라(평면 그래프), 잘 만든 알고리즘은
\(O(N\log N)\)입니다. 다음 강의에서 증분 델로네 를 빈 외접원 뒤집기(flip)로 구현합니다.

예시. 정삼각형 세 꼭짓점의 보로노이 다이어그램은 무게중심에서 세 방향으로 뻗는 세 반직선이고,
쌍대인 델로네는 그 삼각형 하나입니다.

Lesson 증분 델로네 구현과 inCircle 정수 술어 선택 8m

inCircle 술어 — 정수로 정확하게 (C++)

델로네의 심장은 외접원 판정입니다. 좌표가 정수면 __int128로 오차 없이 계산합니다.

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using lll = __int128;

struct P { ll x, y; };
ll cross(P a, P b, P c){ return (b.x-a.x)*(c.y-a.y) - (b.y-a.y)*(c.x-a.x); }

// a,b,c 가 반시계일 때: d 가 외접원 안이면 >0, 원 위면 0, 밖이면 <0
lll inCircle(P a, P b, P c, P d){
    lll ax=a.x-d.x, ay=a.y-d.y, bx=b.x-d.x, by=b.y-d.y, cx=c.x-d.x, cy=c.y-d.y;
    lll a2=ax*ax+ay*ay, b2=bx*bx+by*by, c2=cx*cx+cy*cy;
    return ax*(by*c2 - b2*cy)
         - ay*(bx*c2 - b2*cx)
         + a2*(bx*cy - by*cx);
}

오버플로 경계. 각 항은 좌표 차의 4차식입니다. 좌표가 \(|x|\le10^5\)이면 차는 \(2\times10^5\),
제곱은 \(4\times10^{10}\), 곱은 \(\approx10^{16}\)이라 long long도 위태롭습니다. 좌표가 크면
__int128(위 코드) 또는 좌표를 먼저 평행이동해 절댓값을 줄입니다. 위 코드처럼 \(d\)를 원점으로
당겨 빼면 값이 작아져 안전 여유가 커집니다.


증분 델로네 — flip으로 empty-circle 회복

증분법의 골격은 단순합니다.

  1. 모든 점을 감싸는 아주 큰 가상의 큰 삼각형(super-triangle) 으로 시작한다.
  2. 점을 하나씩 삽입한다. 그 점을 포함하는 삼각형을 찾아 세 개로 쪼갠다.
  3. 새로 생긴 간선들에 대해 델로네 조건 위반(inCircle > 0) 이면 대각선을 뒤집는다(flip).
    뒤집기는 이웃으로 전파되며, 더 이상 위반이 없을 때까지 반복한다.
  4. 모든 점을 넣은 뒤, 가상 삼각형의 꼭짓점과 이어진 삼각형들을 제거하면 델로네 완성.
// flip 판정의 핵심: 인접한 두 삼각형 (a,b,c) 와 (a,c,d) 가 사각형 a-b-c-d 를 이룰 때
// d 가 삼각형 (a,b,c) 의 외접원 안이면 대각선 (a,c) 를 (b,d) 로 뒤집어야 델로네가 된다.
bool shouldFlip(P a, P b, P c, P d){
    if (cross(a, b, c) <= 0) return false;     // (a,b,c) 가 반시계가 아니면 방향 보정 필요
    return inCircle(a, b, c, d) > 0;           // d 가 외접원 안 → flip
}

간선·삼각형을 인접 관계와 함께 관리하려면 보통 반쪽 간선(half-edge) 또는 삼각형 인접 배열로
자료구조를 둡니다. 각 삽입이 평균 상수 개의 flip만 유발하도록 점을 무작위 순서로 넣으면 기대
\(O(N\log N)\)입니다.


파이썬 — inCircle 판정 (오차 없는 정수)

def in_circle(a, b, c, d):
    ax, ay = a[0]-d[0], a[1]-d[1]
    bx, by = b[0]-d[0], b[1]-d[1]
    cx, cy = c[0]-d[0], c[1]-d[1]
    a2, b2, c2 = ax*ax+ay*ay, bx*bx+by*by, cx*cx+cy*cy
    return (ax*(by*c2 - b2*cy)
          - ay*(bx*c2 - b2*cx)
          + a2*(bx*cy - by*cx))   # >0: 안, 0: 원 위, <0: 밖 (a,b,c 반시계 가정)

파이썬 정수는 다정밀도라 오버플로가 없어, 델로네/보로노이의 참조 구현·검증기 로 이상적입니다.


흔한 함정

  • 오버플로가 최대 함정 — inCircle은 좌표의 4차식. __int128 또는 좌표 평행이동으로 대비.
  • 방향 가정 — inCircle은 \(a,b,c\)반시계 임을 전제. 시계면 부호가 뒤집히니 먼저 cross
    방향을 확인/정렬.
  • 공원(cocircular) 4점 이상 — inCircle \(=0\)인 퇴화. 델로네가 유일하지 않게 되므로 일관된
    타이브레이크(예: 좌표 사전순)로 결정론적으로 처리.
  • 공선 사이트 — 세 점이 일직선이면 삼각형이 없어 외접원이 정의 안 됨. 별도 처리.
  • 가상 삼각형 크기 — super-triangle을 충분히 크게 잡되, 정수 좌표를 유지하려면 안전한 큰 상수로.
Lesson 심화·응용 — EMST·최대 공터 원·최근접점 선택 8m

출제 신호

  • "각 지점에서 가장 가까운 사이트/시설", "최근접점 질의를 여러 번".
  • "가장 큰 빈 원(사이트가 하나도 없는 최대 반지름 원)을 도심 안에 놓아라".
  • "점들의 유클리드 최소 신장 트리", "최근접 쌍", "각 점의 최근접 이웃".
  • 이들은 대개 델로네 간선 \(O(N)\) 만 있으면 풀리므로, 보로노이를 직접 그리기보다 델로네를
    만들어 그래프로 넘기는 편이 실전적입니다.

응용 1 — 유클리드 최소 신장 트리 (EMST)

평면 점들의 EMST는 모든 완전그래프 간선이 아니라 델로네 간선만 봐도 충분하다는 정리가 있습니다
(EMST \(\subseteq\) Delaunay). 그래서:

  1. 델로네 삼각분할을 만든다 → \(O(N)\)개의 후보 간선.
  2. 각 간선에 유클리드 거리(제곱)를 얹어 일반 MST(크루스칼/프림)를 돌린다.

전체 \(O(N\log N)\). \(N^2\)개 간선을 다 보는 순진한 방법을 델로네가 \(O(N)\)개로 줄여 줍니다.

// 델로네 간선 목록 edges(사이트 인덱스 쌍)로 EMST 무게 합
ll dist2(P a, P b){ ll dx=a.x-b.x, dy=a.y-b.y; return dx*dx+dy*dy; }
// (u,v,dist2) 로 정렬 후 유니온-파인드 크루스칼 → 표준

거리 비교는 제곱(정수)으로 하고, 실제 길이 합이 필요하면 마지막에 \(\sqrt{}\)를 더합니다.


응용 2 — 최대 공터 원 (largest empty circle)

사이트가 하나도 들어오지 않는 가장 큰 원의 중심은 다음 후보 중 하나입니다.

  • 보로노이 꼭짓점(세 사이트 등거리 = 델로네 삼각형의 외심), 또는
  • 보로노이 변과 도심 경계(볼록 껍질/제약 영역)의 교점.

각 후보에서 가장 가까운 사이트까지의 거리를 재 최댓값을 취합니다. 델로네 외심들만 훑으면 후보가
\(O(N)\)개라 빠릅니다.


응용 3 — 최근접점/최근접 쌍

보로노이 셀은 곧 "이 사이트가 최근접인 영역"이므로, 질의점이 어느 셀에 있는지 점 위치 질의(point
location)
로 찾으면 최근접 사이트를 \(O(\log N)\)에 답할 수 있습니다. 최근접 쌍(가장 가까운 두
점)도 델로네 간선 중 최소 길이 간선으로 얻습니다 — 최근접 쌍은 항상 델로네 간선이기 때문입니다.


함정과 변형 총정리

  • 정밀도 vs 정수. 델로네의 판정(cross, inCircle)은 정수로 정확히 가능하지만, 보로노이 꼭짓점
    좌표(외심)는 유리수/무리수라 실수로 다뤄야 합니다. 비교는 최대한 정수 술어로, 좌표 산출만 실수로.
  • 퇴화. 공선 사이트(삼각분할 불가), 공원 4점(델로네 비유일), 중복 사이트는 사전에 정리하거나
    일관된 타이브레이크로.
  • 경계·무계 셀. 바깥쪽 사이트의 보로노이 셀은 무한히 뻗습니다. 응용에서 도심(볼록 껍질/상자)으로
    잘라 유계화합니다.
  • 직접 구현 부담. 완전한 증분 델로네는 자료구조(반쪽 간선)가 무겁습니다. 대회에서는 필요한
    간선(EMST·최근접)만 뽑는 목적형 구현이나, 검증엔 파이썬 정수 참조 구현을 병행하는 전략이 안전합니다.
  • 변형. 가중 보로노이(파워 다이어그램), 최근접 대신 최원점(farthest-point) 보로노이(최소
    외접원에 사용) 등으로 확장됩니다.
Practice problem 가장 가까운 거점 선택 25m
R00697

가장 가까운 거점

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기