프로그래밍 연습장

Graham Scan 본문

알고리즘

Graham Scan

김준원 2017. 8. 19. 16:33

2차원 좌표평면에 점들이 있을 때, 이들의 컨벡스 헐은 점들을 모두 포함하는 가장 작은 볼록다각형을 뜻한다. "점들이 있는 곳에 못을 박고, 바깥에 엄청 큰 고무줄을 놓았을 때 고무줄이 걸리는 점들"이라는 비유가 좋다. 꼭짓점이 점들의 부분집합이고, 모든 점을 포함하는 볼록다각형이면 컨벡스 헐이다.

 

컨벡스 헐, 정확히 "컨벡스 헐의 꼭짓점에 위치한 점들의 리스트"을 구하는 Graham Scan 알고리즘은 구현이 길지 않다:

1
2
3
4
5
6
7
8
9
sort(a, a+n);
sort(a+1, a+n, [](pint x, pint y) { return ccw(a[0], x, y) > 0 or (ccw(a[0], x, y) == 0 and dist(a[0], x) < dist(a[0], y)); });
 
= 2;
for (int i=2; i<n; i++) {
    while (t>1 and ccw(r[t-2], r[t-1], a[i]) <= 0) t--;
    r[t++= a[i];
}
 
cs

 

다음은 이 알고리즘의 동작 원리를 설명하는 세 줄 요약..이라기 보다는 이 세 줄이 전부다.

 

  1. 점들을 좌표 순서대로 정렬했을 때의 첫 정점은 무조건 컨벡스 헐의 꼭짓점을 이룬다. 즉, X좌표가 가장 작은 점들 중 Y좌표가 가장 작은 점을 찾아, 얘를 극점이라고 하자. 극점은 컨벡스 헐의 꼭짓점에 포함된다.

  2. 나머지 N1개의 점들을 극점에서 시작하는 벡터가 축과 이루는 각도 순으로 정렬한다. 각도가 같다면 2순위로 극점과의 거리의 오름차순이 되게 하자.

  3. 정렬한 순서대로 N1개의 점들을 차례로 보면서, 컨벡스 헐의 꼭짓점 리스트에 넣는다. 무조건 넣는다. 만약 새로운 점을 추가할 때 컨벡스 헐에 오목한 각이 생겨서 못 넣는다면, 오목한 각이 없어질 때까지 마지막으로 추가한 점들을 계속 빼서라도 기어코 넣는다.

 

 

일종의 그리디 알고리즘인데, 올바르게 작동함은 다음과 같이 증명한다. 말장난 같다.

 

 

  • 수학적 귀납법을 쓰자. 극점을 p1, 나머지 점을 각도 순으로 p2..n이라 하자. p1..i의 컨벡스 헐을 Ci라 하자. 물론 S(C1)=0임은 당연하다.

  • Graham Scan 알고리즘으로 Ci1까지를 올바르게 구했다고 하자. 이제 Ci를 구성하기 위해 pi를 추가하려 한다. piCi1에 없으니까, Ci의 꼭짓점에 포함되어야 한다.

  • Ci1에다 pi를 더해 다각형을 만들었는데, pi와 직전에 추가한 두 점이 이루는 각 빼곤 다 볼록함을 알고 있다. 만약 새로 생긴 각이 오목하다면, 직전에 추가한 점을 제거해도 모든 점을 아직 포함하고 있다. 그 과정을 반복하다 보면 볼록해지고, 컨벡스 헐이 되고, Ci가 된다.

 

그리고 구현할 때 기억할 점. 기하 알고리즘이 다 그렇듯이 이상한 곳에서 뻑나기 십상이다.

 

1. 문제에서 한 직선 위에 여러 점이 있을 수 없다는 조건을 주지 않는 이상, 무조건 거리의 오름차순으로 정렬해야 한다. 까먹어도 대부분의 경우엔 상관 없지만, 첫 번째로 추가하는 점이 극점과 이루는 직선에 여러 점이 있을 때 오작동할 수 있다.

 

2. 위 6번 줄의 ccw 조건에서 등호를 빼면 180'의 내각을 이루는 꼭짓점도 모두 포함하게 된다. 즉 컨벡스 헐의 경계 위에 있는 모든 점을 구할 수 있게 된다. 문제에 따라서 알아서 쓰면 된다. 하지만 이 때에는 이 후처리를 잊으면 안 된다.

  • 마지막 점(Pn)이 극점(P1)과 이루는 직선 위의 점들은 거리가 감소하는 순서대로 추가되어야 한다. 따라서 모두 추가된 뒤에 이 점들의 순서를 뒤집는 처리를 해야 옳은 순서가 된다.

이를 포함한 "컨벡스 헐 위의 경계 위에 있는 모든 점을 구하는" 코드는 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sort(a, a+n);
sort(a+1, a+n, [](pint x, pint y) { return ccw(a[0], x, y) > 0 or (ccw(a[0], x, y) == 0 and dist(a[0], x) < dist(a[0], y)); });
 
for (int i=n-1; i; i--if (ccw(a[0], a[i], a[i-1])) {
    reverse(a+i, a+n);
    break;
}
 
= 2;
for (int i=2; i<n; i++) {
    while (t>1 and ccw(r[t-2], r[t-1], a[i]) < 0) t--;
    r[t++= a[i];
}
 
cs

 

3. ccw 함수

1
2
3
4
5
6
7
int ccw(pint a, pint b, pint c) {
    lint k = a.x * (lint)b.y + b.x * (lint)c.y + c.x * (lint)a.y
            -a.y * (lint)b.x - b.y * (lint)c.x - c.y * (lint)a.x;
    if (k>0return 1;
    if (k<0return -1;
    return 0;
}
cs

에서 정수 오버플로우가 나지 않게 64비트 정수형으로 캐스팅해주자. 그냥 좌표 자체를 처음부터 넉넉하게 64비트로 선언하는 게 제일 편하다.

 

 

Comments