볼록 껍질 ⑤ — 가장 먼 두 점과 Rotating Calipers

1편부터 4편까지, 우리는 흩어진 점들의 볼록 껍질을 구하는 네 가지 방법을 봤다. Package Wrapping, Graham Scan, Plane Sweeping, 그리고 분할 정복. 마지막 편은 방향을 바꾼다. 껍질을 도구로 쓴다. 평면에서 가장 멀리 떨어진 두 점, 곧 점들의 지름(diameter) 을 찾는 문제인데, 모든 쌍을 재보는 O(N2)O(N^2) 대신 껍질을 구한 뒤 O(N)O(N)에 훑는 Rotating Calipers로 전체 O(NlogN)O(N \log N)에 푼다.

이 포스트에서 다루는 내용
  • 1차원에서 2차원으로 — 왜 어려워지나
  • 가장 먼 두 점은 껍질 꼭짓점이다
  • 평행선 회전과 대척점 쌍(antipodal pair)
  • Rotating Calipers로 대척점만 O(N)O(N)에 훑기

이번에도 껍질은 1~4편에서처럼 일반 위치(한 직선 위에 점이 3개 이상 없음)를 가정하고, 이미 반시계 순서로 구해 두었다고 하자.


1차원에서 2차원으로

가장 먼 두 점을 찾는 문제는 1차원에선 시시하다. 수직선 위의 점들 중 가장 먼 쌍은 최솟값과 최댓값이니, 한 번 훑어 O(N)O(N)에 끝난다.

평면은 다르다. 어느 두 점이 가장 먼지 미리 알 수 없어, 소박하게는 모든 쌍의 거리를 재야 한다. 점이 NN개면 쌍이 약 N2/2N^2/2개라 O(N2)O(N^2). 점이 많아지면 버겁다. 더 좋은 방법이 있을까? 볼록 껍질을 쓰면 후보를 크게 줄일 수 있다.


가장 먼 두 점은 껍질 꼭짓점이다

첫 관찰은 이것이다. 가장 먼 두 점은 반드시 볼록 껍질의 꼭짓점이다.

qq를 하나 고정하고 함수 f(x)=xq2f(x) = |x - q|^2을 보자. 이 함수는 볼록이다. 그리고 볼록 함수가 볼록 다각형 위에서 갖는 최댓값은 반드시 꼭짓점에서 나온다.

이유는 이렇다. 다각형 안의 점 xx는 꼭짓점들의 볼록 결합, 곧 x=λkvkx = \sum \lambda_k v_k (λk0\lambda_k \ge 0, λk=1\sum \lambda_k = 1)로 적힌다. 볼록 함수는 f(λkvk)λkf(vk)maxkf(vk)f(\sum \lambda_k v_k) \le \sum \lambda_k f(v_k) \le \max_k f(v_k)를 만족하므로, 어떤 점의 값도 꼭짓점 값의 최댓값을 넘지 못한다.

qq를 아무 점으로 잡아도 성립하므로, 가장 먼 쌍의 한쪽을 고정하면 다른 쪽은 꼭짓점이다. 양쪽에 차례로 적용하면 둘 다 꼭짓점이다. 직관으로 말하면 내부 점은 바깥으로 밀수록 멀어지므로 후보가 되지 못한다는 뜻이다.

가장 먼 두 점은 껍질 꼭짓점 — 내부 점은 바깥으로 밀수록 멀어지므로 후보가 못 된다.
가장 먼 두 점은 껍질 꼭짓점 — 내부 점은 바깥으로 밀수록 멀어지므로 후보가 못 된다.

그래서 전략이 선다. 먼저 볼록 껍질을 구해(1~4편의 어느 방법이든 O(NlogN)O(N \log N)) 후보를 껍질 꼭짓점으로 좁힌다. 남은 일은 껍질 꼭짓점들 중 가장 먼 쌍을 찾는 것이다.


평행선 회전과 대척점 쌍

껍질 꼭짓점만 봐도 그 쌍을 다 재면 다시 O(H2)O(H^2)(HH는 꼭짓점 수)다. 여기서 기하적 그림 하나가 볼 쌍의 수를 확 줄인다.

볼록 다각형을 평행한 두 직선으로 양쪽에서 떠받친다고 하자(자를 양옆에 대는 느낌). 두 직선은 각각 다각형에 한 점(또는 한 변)에서 닿는다. 이렇게 마주 보며 닿는 점 쌍을 대척점 쌍(antipodal pair) 이라 부른다. 이때 두 점을 잇는 선분은 지지선과 수직일 필요가 없다.

이제 평행선의 각도를 0°에서 180°까지 돌린다. 각도가 바뀔 때마다 닿는 대척점 쌍도 바뀐다. 핵심은 이것이다. 가장 먼 두 점은 반드시 어떤 각도에서 대척점 쌍으로 나타난다. 가장 먼 쌍 aa, bb를 잇는 선분에 수직인 방향으로 평행선을 대면 두 직선이 정확히 aabb에 닿기 때문이다. 그러니 모든 대척점 쌍만 훑어 그중 최대 거리를 고르면 그게 지름이다. 대척점이 아닌 쌍은 볼 필요조차 없다.

지름과 대척점 쌍 — 지름에 수직인 두 평행 지지선은 정확히 그 두 끝점에 닿는다. 그래서 지름은 어떤 각도에서 대척점 쌍으로 나타난다. (일반적인 대척점 쌍의 선분은 지지선과 수직이 아닐 수 있다.)
지름과 대척점 쌍 — 지름에 수직인 두 평행 지지선은 정확히 그 두 끝점에 닿는다. 그래서 지름은 어떤 각도에서 대척점 쌍으로 나타난다. (일반적인 대척점 쌍의 선분은 지지선과 수직이 아닐 수 있다.)

Rotating Calipers — O(N)O(N)

남은 문제는 “평행선을 연속으로 돌린다”를 어떻게 이산적인 알고리즘으로 옮기느냐다. 다행히 대척점 쌍은 각도가 돌면서 껍질 위를 한 방향으로만 이동한다. 그래서 직선을 직접 돌리는 대신, 껍질 위에 두 포인터를 두고 그것들을 회전시키면 된다. 물체를 양쪽에서 재는 자(caliper) 두 짝을 벌려 돌리는 그림이라 Rotating Calipers다.

한 걸음은 이렇다. 두 포인터가 각각 위·아래 지지선의 접점이라 하자. 각 접점에서 다음 변으로 넘어가려면 지지선을 얼마나 돌려야 하는지 비교해, 더 작은 각도로 벌어진(먼저 닿는) 쪽 포인터를 다음 꼭짓점으로 전진시킨다. 전진할 때마다 마주친 대척점 쌍의 거리를 재 최댓값을 갱신한다.

Rotating Calipers 한 걸음 — 두 포인터 중 더 작은 각도로 벌어진 쪽을 다음 꼭짓점으로 전진시킨다.
Rotating Calipers 한 걸음 — 두 포인터 중 더 작은 각도로 벌어진 쪽을 다음 꼭짓점으로 전진시킨다.

각도 비교는 삼각함수 없이 외적(cross product) 으로 한다. 외적의 크기는 한 점이 어떤 변(직선)에서 얼마나 떨어져 있는지에 비례하므로, 다음 꼭짓점이 현재 변에서 더 멀어지는 동안 포인터를 밀면 그 변의 대척점에 닿는다(1편 CCW와 같은 도구).

// hull: 볼록 껍질 꼭짓점(반시계 순서) 배열, H개. struct P·cross는 1편 정의.
// cross(O, A, B): 벡터 OA × OB (부호 있는 넓이의 2배)
// dist2(A, B): 제곱거리 — 부동소수 없이 비교하려고 제곱으로 둔다
long long best = 0;
int j = 1;
for (int i = 0; i < H; i++) {
    int ni = (i + 1) % H;                 // 변 i→ni
    // 변 i→ni에서 가장 먼 꼭짓점(대척점)까지 j를 전진
    while (cross(hull[i], hull[ni], hull[(j + 1) % H])
             > cross(hull[i], hull[ni], hull[j])) {
        j = (j + 1) % H;
    }
    // (i, j)와 (ni, j)가 대척점 쌍 후보
    best = max(best, dist2(hull[i], hull[j]));
    best = max(best, dist2(hull[ni], hull[j]));
}
// best = 지름의 제곱. 실제 지름은 sqrt(best).
엣지 케이스는 추가 설명에서

위 의사코드는 기본 뼈대이고 전제가 몇 가지 있다.

  • 껍질 꼭짓점이 3개 이상이다(H3H \ge 3). 점이 모두 같으면 H=1H = 1이라 지름이 00이고, 모두 한 직선 위면 H=2H = 2라 양 끝 두 점이 답이다. 두 경우는 루프를 돌리지 않고 따로 처리한다.
  • 평행한 변이 없다. 1편의 「세 점이 한 직선 위에 있지 않다」로는 이 조건이 따라오지 않는다. 서로 다른 두 변이 나란한 것은 얼마든지 가능하고, 정사각형이 그렇다. > 비교가 변마다 대척점을 딱 하나로 고르는 것은 이 전제 위에서다.
  • 정수 범위. crossdist2가 좌표 곱을 담으므로 long long으로 받는다. 좌표 절댓값이 CC 이하이면 dist2는 최대 8C28C^2, cross는 최대 8C28C^2 수준이라 C=109C = 10^9까지는 안전하다. 범위를 모르는 입력이면 __int128이 필요하다. 평행한 두 이 동시에 닿아 한 각도에서 대척점 쌍이 여럿 생기는 동률에서는, 포인터를 언제 둘 다 움직여야 하는지, 그리고 대척점 쌍을 빠짐없이 세는 종료 조건이 갈린다. 이 완전한 열거는 곧 이어질 추가 설명 글에서 따로 정리한다. 여기서는 “두 포인터를 더 작은 각도 쪽으로 전진시켜 한 바퀴 돈다”는 뼈대만 가져간다.

복잡도

두 포인터는 각각 껍질을 한 바퀴만 돈다. ii는 for 문으로 HH번, jj는 while 문에서 전진하되 뒤로는 가지 않아 전체를 통틀어 HH번을 넘지 못한다. 그러니 대척점 순회는 O(H)O(H)이고, 껍질 크기가 최대 NN이니 O(N)O(N)이다.

여기에 껍질을 구하는 O(NlogN)O(N \log N)을 더하면 전체 O(NlogN)O(N \log N). 모든 쌍을 재는 O(N2)O(N^2)과 비교하면, 껍질을 먼저 구하느라 들인 비용이 톡톡히 회수된다. 껍질은 가장 먼 점뿐 아니라 폭(width)·최소 외접 사각형 같은 여러 극값 문제의 공통 발판이 된다.


핵심 정리
  • 평면에서 가장 먼 두 점(지름) 은 반드시 볼록 껍질의 꼭짓점이다. 내부 점은 밖으로 밀수록 멀어지기 때문이다.
  • 평행한 두 지지선을 돌리며 닿는 대척점 쌍만 보면 된다. 가장 먼 쌍은 어떤 각도에서 반드시 대척점 쌍으로 나타난다.
  • Rotating Calipers는 직선 대신 껍질 위 두 포인터를 회전시켜, 더 작은 각도로 벌어진 쪽을 전진하며 대척점 쌍을 O(N)O(N)에 순회한다. 각도 비교는 외적으로 한다.
  • 껍질 O(NlogN)O(N \log N) + 순회 O(N)O(N) = 전체 O(NlogN)O(N \log N). 모든 쌍 O(N2)O(N^2)보다 빠르다.
시리즈를 마치며

다섯 편에 걸쳐 볼록 껍질을 구하는 네 가지 길(Package Wrapping · Graham Scan · Plane Sweeping · 분할 정복)과, 그렇게 구한 껍질을 도구로 쓰는 법(가장 먼 두 점)을 살펴봤다. 껍질은 그 자체가 답인 동시에, 여러 기하 문제를 푸는 발판이다.

Rotating Calipers의 엣지 케이스(평행 변·대척점 쌍의 정확한 열거)는 추가 설명 글에서 마저 다룬다. 계산 기하의 이웃 문제인 가장 가까운 점 쌍도 함께 보면 좋다.

© 2026 XsQuare01. Powered by GitHub Pages. · 방문자