Post

23. CCW & 볼록 껍질

23. CCW & 볼록 껍질

개념

CCW(Counter-Clockwise): 세 점의 방향 관계를 판별하는 기초 연산. 볼록 껍질(Convex Hull): 주어진 점들을 모두 포함하는 가장 작은 볼록 다각형.

핵심 특징

  • CCW: 외적의 부호로 반시계/시계/일직선 판별
  • 볼록 껍질: 2D 점 집합의 외곽선 계산
  • 기하 알고리즘의 기초: 선분 교차, 다각형 면적 등에 활용

CCW (Counter-Clockwise)

원리

1
2
3
4
5
6
7
8
9
세 점 A, B, C에 대해
벡터 AB = (B.x - A.x, B.y - A.y)
벡터 AC = (C.x - A.x, C.y - A.y)

외적 = AB × AC = (B.x-A.x)*(C.y-A.y) - (B.y-A.y)*(C.x-A.x)

외적 > 0: 반시계 방향 (CCW)
외적 = 0: 일직선 (Collinear)
외적 < 0: 시계 방향 (CW)

구현

1
2
3
4
5
6
7
8
struct Point {
    long long x, y;
};

// 양수: 반시계, 0: 일직선, 음수: 시계
long long ccw(Point A, Point B, Point C) {
    return (B.x - A.x) * (C.y - A.y) - (B.y - A.y) * (C.x - A.x);
}

볼록 껍질 (Convex Hull)

그라함 스캔

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <bits/stdc++.h>
using namespace std;

struct Point {
    long long x, y;
};

Point pivot;

long long ccw(Point A, Point B, Point C) {
    return (B.x - A.x) * (C.y - A.y) - (B.y - A.y) * (C.x - A.x);
}

long long dist2(Point A, Point B) {
    return (A.x - B.x) * (A.x - B.x) + (A.y - B.y) * (A.y - B.y);
}

bool cmp(Point& a, Point& b) {
    long long v = ccw(pivot, a, b);
    if (v != 0) return v > 0;  // 반시계 방향 우선
    return dist2(pivot, a) < dist2(pivot, b);  // 가까운 점 우선
}

vector<Point> grahamScan(vector<Point>& pts) {
    int n = pts.size();

    // 가장 아래, 왼쪽 점 찾기
    int minIdx = 0;
    for (int i = 1; i < n; i++) {
        if (pts[i].y < pts[minIdx].y ||
            (pts[i].y == pts[minIdx].y && pts[i].x < pts[minIdx].x)) {
            minIdx = i;
        }
    }
    swap(pts[0], pts[minIdx]);
    pivot = pts[0];

    // 각도 기준 정렬
    sort(pts.begin() + 1, pts.end(), cmp);

    // 스택으로 볼록 껍질 구성
    vector<Point> hull;
    for (auto& p : pts) {
        while (hull.size() >= 2 &&
               ccw(hull[hull.size()-2], hull[hull.size()-1], p) <= 0) {
            hull.pop_back();
        }
        hull.push_back(p);
    }

    return hull;
}

모노톤 체인

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <bits/stdc++.h>
using namespace std;

struct Point {
    long long x, y;
    bool operator<(const Point& o) const {
        return x < o.x || (x == o.x && y < o.y);
    }
};

long long ccw(Point A, Point B, Point C) {
    return (B.x - A.x) * (C.y - A.y) - (B.y - A.y) * (C.x - A.x);
}

vector<Point> monotoneChain(vector<Point>& pts) {
    int n = pts.size();
    if (n < 3) return pts;

    sort(pts.begin(), pts.end());

    vector<Point> hull;

    // 아래 껍질 (Lower Hull)
    for (auto& p : pts) {
        while (hull.size() >= 2 &&
               ccw(hull[hull.size()-2], hull[hull.size()-1], p) <= 0) {
            hull.pop_back();
        }
        hull.push_back(p);
    }

    // 위 껍질 (Upper Hull)
    int lower_size = hull.size();
    for (int i = n - 2; i >= 0; i--) {
        while ((int)hull.size() > lower_size &&
               ccw(hull[hull.size()-2], hull[hull.size()-1], pts[i]) <= 0) {
            hull.pop_back();
        }
        hull.push_back(pts[i]);
    }

    hull.pop_back();  // 마지막 점 (시작점과 중복) 제거
    return hull;
}

선분 교차 판정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
struct Point {
    long long x, y;
};

long long ccw(Point A, Point B, Point C) {
    return (B.x - A.x) * (C.y - A.y) - (B.y - A.y) * (C.x - A.x);
}

bool onSegment(Point p, Point a, Point b) {
    return min(a.x, b.x) <= p.x && p.x <= max(a.x, b.x) &&
           min(a.y, b.y) <= p.y && p.y <= max(a.y, b.y);
}

// 선분 AB와 선분 CD의 교차 판정
bool intersects(Point A, Point B, Point C, Point D) {
    long long d1 = ccw(A, B, C);
    long long d2 = ccw(A, B, D);
    long long d3 = ccw(C, D, A);
    long long d4 = ccw(C, D, B);

    if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) &&
        ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))) {
        return true;
    }

    // 일직선 위의 경우
    if (d1 == 0 && onSegment(C, A, B)) return true;
    if (d2 == 0 && onSegment(D, A, B)) return true;
    if (d3 == 0 && onSegment(A, C, D)) return true;
    if (d4 == 0 && onSegment(B, C, D)) return true;

    return false;
}

사용 예시

다각형의 면적 (Shoelace Formula)

1
2
3
4
5
6
7
8
9
10
11
12
13
// 볼록/오목 다각형 모두 가능
double polygonArea(vector<Point>& pts) {
    long long area2 = 0;
    int n = pts.size();

    for (int i = 0; i < n; i++) {
        int j = (i + 1) % n;
        area2 += pts[i].x * pts[j].y;
        area2 -= pts[j].x * pts[i].y;
    }

    return abs(area2) / 2.0;
}

볼록 껍질 넓이

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <bits/stdc++.h>
using namespace std;

int main() {
    int N;
    cin >> N;

    vector<Point> pts(N);
    for (int i = 0; i < N; i++) cin >> pts[i].x >> pts[i].y;

    vector<Point> hull = monotoneChain(pts);

    // Shoelace로 넓이 계산
    long long area2 = 0;
    int n = hull.size();
    for (int i = 0; i < n; i++) {
        int j = (i + 1) % n;
        area2 += hull[i].x * hull[j].y;
        area2 -= hull[j].x * hull[i].y;
    }

    cout << fixed << setprecision(1) << abs(area2) / 2.0 << '\n';
    return 0;
}

주의사항 / Edge Cases

정수 오버플로우

1
2
3
// 좌표가 10^6이면 외적 값이 10^12 → long long 필수
long long ccw = (long long)(B.x - A.x) * (C.y - A.y)
              - (long long)(B.y - A.y) * (C.x - A.x);

일직선 위의 점

1
2
// ccw = 0인 경우 별도 처리 필요
// 볼록 껍질: <= 0 (일직선 점 제외) vs < 0 (일직선 점 포함)

점이 2개 이하

1
2
// 볼록 껍질은 최소 3개의 점이 필요
// 모든 점이 일직선이면 볼록 껍질이 선분

부동소수점 오차

1
2
// 가능하면 정수 연산으로 처리
// double을 써야 하면 EPS = 1e-9 정도로 비교

면접 포인트

자주 나오는 질문

Q1. CCW란?

  • 세 점의 방향 관계를 판별하는 연산
  • 외적의 부호로 반시계(+), 시계(-), 일직선(0) 판별
  • 기하 알고리즘의 가장 기본 연산

Q2. 볼록 껍질의 시간복잡도는?

  • 정렬: O(N log N), 스캔: O(N)
  • 전체: O(N log N)

Q3. 그라함 스캔 vs 모노톤 체인?

  • 그라함 스캔: 각도 정렬, 하나의 스캔
  • 모노톤 체인: x좌표 정렬, 상/하 두 번 스캔
  • 모노톤 체인이 구현이 더 간단하고 안정적

Q4. 선분 교차 판정에서 CCW를 사용하는 이유는?

  • 두 선분이 교차하려면 한 선분의 양 끝점이 다른 선분 기준으로 반대편에 있어야 함
  • CCW 부호가 다르면 반대편 → 교차

코드 체크리스트

1
2
3
4
5
// 1. Point 구조체 (long long 사용)
// 2. CCW 함수 구현
// 3. 정렬 (각도 or x좌표)
// 4. 스택으로 볼록 껍질 구성
// 5. 오버플로우 주의

추천 문제

난이도문제링크핵심
GoldCCWBOJ 11758CCW 기본
Gold선분 교차 1BOJ 17386선분 교차
Gold선분 교차 2BOJ 17387경계 포함 교차
Platinum볼록 껍질BOJ 1708기본 볼록 껍질
Platinum로버트 후드BOJ 9240볼록 껍질 + 회전 캘리퍼스
Gold다각형의 면적BOJ 2166Shoelace
This post is licensed under CC BY 4.0 by the author.