직관적으로 설명하면, 평면 상의 볼록 껍질은 여러 점들의 집합이 주어졌을 때, 이 집합의 일부만을 꼭짓점으로 가지면서 모든 점들을 감싸는 볼록 다각형의 꼭짓점 집합 중 가장 작은 것을 말한다.
아래와 같은 점들이 있다고 하자.
이 점들의 일부를 아래 그림의 왼쪽과 같이 이으면 볼록 껍질을 되며, 오른쪽과 같이 잇는 경우는 모든 점들을 감쌀 수는 있지만 오목 다각형이기 때문에 볼록 껍질이 아니다.
한편 아래 그림에서(제일 오른쪽에 있는 세 점이 일직선을 이룬다고 하자), 왼쪽은 볼록 껍질이지만 오른쪽은 아니다. 이 둘은 모두 주어진 모든 점들을 포함하는 볼록 다각형을 만들고는 있지만, 볼록 껍질은 그러기 위한 최소 개수의 점 집합이어야 하기 때문이다.
점들이 주어졌을 때 볼록 껍질을 찾는 대표적인 알고리즘 중 하나가 그레이엄 스캔이다.
그레이엄 스캔은 다음의 순서로 진행한다.
그림과 함께 살펴보자.
아래 그림에서 주황색으로 색칠된 점이 가장 작은 y좌표를 가지고 있다. 이 점을 P라고 하자.
앞서 말한 것과 같이 각 점과 P가 이루는 각도를 직접 계산할 필요는 없다. CCW를 이용하면 각 각도를 따로 직접 계산하지 않고도 상대적인 각도를 알 수 있기 때문이다. 다음의 예를 보자.
그림에서 점 A, P와 x축이 이루는 각도는 점 B, P가 x축과 이루는 각도보다 작다. 이러한 관계는 아래와 같이 점 P, A, B가 순서대로 반시계 방향을 이루는지를 확인함으로써 파악할 수 있다.
만약 B가 아래 그림과 같이 다른 곳에 위치해 있었다고 해보자.
이 경우 점 A, P와 x축이 이루는 각도는 점 B, P와 x축이 이루는 각도보다 크며, 우리는 앞서와는 달리 점 P, A, B가 순서대로 시계 방향을 이루고 있음을 확인할 수 있다.
한편 아래와 같이 점 P, x축과 이루는 각도가 동일한 두 점이 있을 수도 있다. 이 경우에는 해당하는 점들과 P 사이의 거리를 기준으로 정렬한다. 그 이유는 같은 직선 위에 있는 두 점 중 점 P에 더 가까운 점은 절대 볼록 껍질에 들어가지 않기 때문이다. 이와 같은 경우, 다음의 단계에서 만약 점 P로부터 먼 점을 먼저 체크하게 된다면 볼록 껍질에 들어갔어야 할 점이 제외되는 문제가 생길 수도 있다.
정렬이 완료된 후의 결과는 다음과 같다.
점 P부터 시작해, 각 점에 대해 이 점이 스택에 넣은 가장 위쪽의 두 점과 어떤 방향을 이루는지를 확인한다.
점 P와 1번 점을 확인할 때에는 스택에 2개 이상의 점이 없으므로 일단 집어 넣는다.
2번 점을 스택에 넣을지를 판단하기 위해서는, 점 P, 1, 2가 반시계 방향을 이루는지를 확인해야 한다. 점 P, 1, 2가 순서대로 반시계 방향을 이루므로 점 2를 스택에 넣는다.
점 3의 경우에도 마찬가지로 점 1, 2, 3이 순서대로 반시계 방향을 이루는지를 확인해야 한다. 그런데 아래 그림에서 확인할 수 있듯 이 세 점은 시계 방향을 이룬다. 따라서 스택의 가장 위에 있는 점 2는 스택에서 제거된다.
이후 3번을 스택에 넣을지는 스택에 남아있는 점 P, 1을 통해서 다시 판정한다. 점 P, 1, 3이 순서대로 반시계 방향을 이루므로 3을 스택에 넣는다.
이후에도 마찬가지로 진행해 최종적으로는 다음과 같이 볼록 껍질에 속하는 점들을 모두 찾을 수 있다.
그레이엄 스캔은 세 가지의 단계로 나눠져 있었다. 각 단계의 시간 복잡도는 다음과 같다.
결론적으로 그레이엄 스캔은 의 시간 복잡도를 가진다.
typedef pair<int, int> Point;
vector<Point> vertex;
stack<int> stk;
//점 P를 찾는다.
bool compareY(Point a, Point b) {
if (a.second == b.second) return a.first < b.first;
else return a.second < b.second;
}
long long CCW(Point a, Point b, Point c) {
return 1LL * (b.first - a.first) * (c.second - b.second) - 1LL * (b.second - a.second) * (c.first - b.first);
}
long long calcDist(Point a){
return 1LL * (a.first - vertex[0].first) * (a.first - vertex[0].first) + (a.second - vertex[0].second) * (a.second - vertex[0].second);
}
bool compareAngle(Point a, Point b) {
long long ccw = CCW(vertex[0], a, b);
if (ccw) return ccw > 0;
return calcDist(a) < calcDist(b);
}
int main() {
//input 관련 처리들을 통해 vertex에 해당 점들이 입력되었다고 가정.
//graham algorithm
// 1단계 : 가장 y좌표가 작은 점을 찾기.(정렬하지 않는 다른 방법도 당연히 가능)
sort(vertex.begin(), vertex.end(), compareY); //
// 2단계 : vertex[0]은 y좌표가 가장 작은 점 P이다. 이 점 P를 제외한 다른 점들을 P를 기준으로 반시계 방향으로 정렬한다.
sort(vertex.begin() + 1, vertex.end(), compareAngle);
// 3단계
//0번째 점, 즉 점 P와 1번째 점은 비교할 점들이 없으므로 일단 스택에 집어넣는다.
stk.push(0);
stk.push(1);
//2번째 점부터 본다.
int next = 2;
while (next < N) {
while (stk.size() >= 2) {
int second = stk.top();
stk.pop();
int first = stk.top();
if (CCW(vertex[first], vertex[second], vertex[next]) > 0) {
stk.push(second);
break;
}
}
stk.push(next++);
}
//스택의 사이즈는 볼록 껍질의 크기와 같다
printf("%ld", stk.size());
}