본문으로 건너뛰기
김신건의 로그

각도 정렬 (Angle Sorting)

· 수정 · 📖 약 4분 · 1,097자/단어 #algorithm #geometry #angle-sorting
angle sorting, 각도 정렬, polar sort, sort by angle

정의

각도 정렬 (Angle Sorting / Polar Sort) 은 2차원 평면 위의 점들을 기준점에 대한 극각 (polar angle) 순서로 정렬하는 기법. atan2 함수를 이용한 실수 비교 또는 CCW (외적) 를 이용한 정수 비교로 구현. 컨벡스 헐의 Graham scan, 점들을 단순 다각형으로 연결하는 문제 등에서 핵심 구성 요소.

문제 상황과 동기

N 개의 점을 기준점 O 를 기준으로 반시계 방향 (또는 시계 방향)으로 정렬해야 할 때.

  • Naive (atan2): 각 점에 대해 atan2(y, x) 를 계산해 실수 키로 정렬. O(N log N). 간단하지만 부동소수점 오차와 사분면 경계 처리 필요.
  • CCW comparator: ccw(O, a, b) > 0 만으로 정렬. 모든 연산이 정수. strict weak ordering 을 만족시키려면 사분면 구분이 필수.

핵심 통찰: 기준점 O 를 기준으로 점들을 반평면 (upper/lower) 으로 나누고, 같은 반평면 내에서만 CCW 비교 하면 strict weak ordering 이 성립하며, atan2 없이 정수 연산만으로 정확한 각도 정렬이 가능.

시각화

핵심 아이디어

atan2 방식

sort by atan2(y - O.y, x - O.x)
장점: 직관적, 구현 단순
단점: double 오차, atan2 비용, inf/nan 처리

CCW comparator 방식

bool cmp(a, b):
    // 1. 사분면이 다르면 사분면 기준 정렬
    if quadrant(a) != quadrant(b):
        return quadrant(a) < quadrant(b)
    // 2. 같은 사분면이면 CCW
    if ccw(O, a, b) != 0:
        return ccw(O, a, b) > 0
    // 3. 각도가 같으면 거리순
    return dist(O, a) < dist(O, b)

사분면 판정

quadrant(p):
    if p.y == 0 and p.x >= 0: return 0   // +x축
    if p.y > 0 or (p.y == 0 and p.x < 0): return 1  // upper half
    return 2  // lower half (including -x axis and negative y)

또는 구사과의 관용 코드:

if (half(a) != half(b)) return half(a) < half(b)
if (ccw(a, b) != 0) return ccw(a, b) > 0
return dist2(a) < dist2(b)

여기서 half(p) = (p.y > 0) or (p.y == 0 and p.x < 0).

알고리즘

INPUT: points P[0..N-1], origin O (default 0,0)
OUTPUT: P sorted by polar angle (CCW)

1. 각 점 P[i] 에 대해 quadrant = half(P[i] - O) 계산
2. sort P by key:
   a. quadrant first (upper half < lower half)
   b. if same quadrant: ccw(O, a, b) > 0 (반시계 우선)
   c. if collinear: euclidean distance squared ascending

구현

// CCW 기반 각도 정렬 (구사과 스타일)
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
struct Point { ll x, y; };
int ccw(Point a, Point b, Point c) {
  ll ret = (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
  if (ret > 0) return 1;
  if (ret < 0) return -1;
  return 0;
}
ll dist2(Point a, Point b) {
  ll dx = a.x - b.x, dy = a.y - b.y;
  return dx * dx + dy * dy;
}
// upper half = y>0 or (y==0 && x<0)
bool half(Point p) {
  return p.y > 0 || (p.y == 0 && p.x < 0);
}
int main() {
  int n; cin >> n;
  vector<Point> P(n);
  for (auto& p : P) cin >> p.x >> p.y;
  Point O = P[0];  // 기준점 (보통 최하단 좌측)
  // O 가 첫 원소가 되도록 swap 한 후 나머지만 정렬
  // 여기서는 O 를 기준으로 모두 정렬
  sort(P.begin(), P.end(), [&](Point a, Point b) {
      if (half(a) != half(b)) return half(a) > half(b);
      if (ccw(O, a, b) != 0) return ccw(O, a, b) > 0;
      return dist2(O, a) < dist2(O, b);
  });
  for (auto& p : P)
      cout << p.x << " " << p.y << "\n";
}
stdin
4
0 0
1 0
0 1
-1 0
결과
0 0
0 1
-1 0
1 0

복잡도

항목
정렬 시간O(N log N)
CCW 비용O(1) (정수 곱셈 2회)
공간O(N)

변형 / 활용

  1. Graham scan (Convex Hull): 최하단-최좌측 점 기준 각도 정렬 후 스택으로 CCW > 0 인 점만 유지.
  2. 단순 다각형 생성 (BOJ 3679): 각도 정렬로 항상 교차 없는 star-shaped 다각형.
  3. 시계 정렬: CCW 반환값 부호 반전.
  4. 중심 기준 정렬: CCW comparator 의 O 만 변경.

함정

  1. strict weak ordering: ccw(O, a, b) > 0 만 반환하면 non-transitive. 사분면 분할 필수.
  2. 기준점과 동일 점: O 와 같은 점은 첫 번째로 분리.
  3. Collinear: 각도가 같으면 거리순 정렬. 용도에 따라 가까운/먼 순 선택.
  4. atan2 문제: [-pi, pi] 반환 → [0, 2pi) 변환 필요. double 비교가 strict weak ordering 깨뜨릴 수 있음.

BOJ 연습 문제

번호제목정답률링크
BOJ 1708볼록 껍질28.116%kokoa-lab
BOJ 3679단순 다각형26.233%kokoa-lab
BOJ 2699격자점 컨벡스헐57.516%kokoa-lab

참고

이 글의 용어 (4개)
(오목 포함) 다각형 내부 점 판정 (Point in Polygon)algorithm
정의 다각형 내부 점 판정 (Point in Polygon, PIP) 은 단순 다각형 (볼록 또는 오목) 과 하나의 점이 주어졌을 때, 점이 내부/경계/외부 중 어디에 있는지 판…
볼록 껍질 (Convex Hull)algorithm
정의 볼록 껍질 (Convex Hull) 은 주어진 점 집합 P 를 모두 포함하는 최소 크기의 볼록 다각형. 즉 P 의 어떤 점도 다각형 외부에 있지 않고, 다각형의 꼭짓점은 P…
볼록 다각형 내부 점 판정 (Point in Convex Polygon)algorithm
정의 볼록 다각형 내부 점 판정 은 볼록 다각형(convex polygon) 과 하나의 점이 주어졌을 때, 점이 다각형의 내부/경계/외부 중 어디에 있는지 O(log N) 에 판…
Geometry Basic: 벡터, CCW, 외적, 내적algorithm
정의 기하 알고리즘의 밑바탕. 대부분의 2D/3D 기하 문제는 벡터 연산, 내적/외적, CCW 판정 세 가지의 조합으로 풀립니다. 벡터 2D 벡터: 두 점 $A = (ax, ay…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기