CCW
CCW는 평면 위의 세 점 가 어떤 모양을 이루고 있는지를 판단하는 알고리즘입니다. 기하 문제에서 자주 사용됩니다. 두 선분이 서로 교차하는지, 다각형 넓이 계산, 볼록 껍질(Convex Hull) 알고리즘 등에서 활용됩니다. 이 글에서는 CCW의 원리와 선분 교차 판별에 어떻게 활용되는지 살펴보겠습니다.
평면 위에서 세 점은 다음과 같은 모양을 만들 수 있습니다.

CCW 알고리즘은 세 점 를 순서대로 이을 때 어떤 방향으로 이어지는지를 판단합니다. 이를 설명하기 위해서 벡터가 필요합니다. 세 점이 다음과 같이 주어졌다고 할 때,
두 벡터 와 를 구할 수 있습니다.
CCW 알고리즘은 이 두 벡터의 외적을 계산하여 세 점의 방향을 판단합니다. 우선 결과만 보면 CCW는 다음과 같이 계산됩니다.
벡터 와 를 쓰는 대신 와 를 쓰는 이유는 기준점이 같아야 각도를 정의하기 편하기 때문입니다. 그리고 두 방식 모두 결과가 동일합니다. 아래는 이의 증명입니다.
이를 정리하면,
자기 자신과의 외적은 외적의 정의상 항상 0이므로 이렇게 정리됩니다. 따라서 와 를 쓰든 와 를 쓰든 결과는 동일합니다. 정리하면, 세 점 의 CCW는 다음과 같이 계산됩니다.
이 값이 양수이면 세 점은 반시계 방향으로 이어지고, 음수이면 시계 방향으로 이어집니다. 그리고 0이면 세 점이 일직선상에 놓여있다는 것을 의미합니다.
원리
CCW 알고리즘의 원리는 벡터의 외적과 관련이 있습니다. 외적은 기본적으로 3차원에서 정의되는 연산입니다. 두 벡터 의 외적은 로 표현되며, 결과는 와 모두에 수직인 벡터입니다. 여기서 와 가 2차원 벡터인 경우, 외적의 결과는 축 방향의 벡터가 됩니다. 이를 수식으로 표현하면 다음과 같습니다.
여기서 은 와 모두에 수직인 단위 벡터입니다. 지금은 2차원에서 생각하고 있으므로 가 됩니다. 그리고 는 와 사이의 각도입니다. CCW 알고리즘에서 저희가 가장 관심있는 것은 이 각도입니다. 외적을 계산하면, 를 제외하고는 항상 양수이므로 의 부호에 따라 CCW의 부호도 바뀌게 됩니다. 여기서 반시계 방향 회전이라면 는 에서 사이의 값을 가지므로 는 양수입니다. 반대로 시계 방향 회전이라면 는 에서 사이의 값을 가지므로 는 음수입니다. 그리고 세 점이 일직선상에 놓여있다면 는 또는 가 되므로 는 0이 됩니다. 즉, CCW의 부호만 보면 세 점이 어떤 방향으로 이어지는지 알 수 있습니다.
using ll = long long;
ll ccw(ll x1, ll y1, ll x2, ll y2, ll x3, ll y3) {
return (x2 - x1) * (y3 - y1)
- (y2 - y1) * (x3 - x1);
}
이 내용을 바탕으로 BOJ 11758번: CCW 문제를 풀 수 있습니다. 단순히 CCW의 결과를 -1, 0, 1로 변환해 출력하면 됩니다.
선분 교차 판별
선분 교차 판별(Line Segment Intersection) 문제는 평면 위의 두 선분이 서로 교차하는지를 판단하는 문제입니다. 두 선분 와 가 교차하면 다음과 같은 모양을 가지고 있습니다.

선분 를 기준으로 와 가 서로 다른 방향에 있어야 합니다. 즉, 와 의 CCW 결과가 서로 다른 부호를 가져야 합니다. 그리고 이렇게만 검사하면 오른쪽 그림과 같은 반례가 생기기 때문에 선분 를 기준으로 와 도 서로 다른 방향에 있는지 검사해야 합니다. 따라서 선분 교차 판별은 다음과 같이 구현할 수 있습니다.
bool isIntersect(pll a, pll b, pll c, pll d) {
ll ab = ccw(a, b, c) * ccw(a, b, d);
ll cd = ccw(c, d, a) * ccw(c, d, b);
return ab < 0 && cd < 0;
}
여기서 주의해야 할 점은 세 개 이상의 점이 일직선상에 놓여있는 경우입니다. 이 경우에는 CCW의 결과가 0이 되기 때문에 주의해야 합니다. BOJ 17386번: 선분 교차 1 문제에서는 세 점이 일직선상에 놓이는 경우가 없으므로 위의 코드로도 정답을 받을 수 있습니다. 하지만, BOJ 17387번: 선분 교차 2 문제에서는 세 개 이상의 점이 일직선상에 놓이는 경우가 있기 때문에 추가적인 처리가 필요합니다. 경우의 수를 나누어서 생각해 보겠습니다. 한 선분의 끝 점이 다른 선분이나 끝 점 위에 올라가는 경우, 즉, 세 점이 일직선상에 놓이는 경우에는 CCW 중 하나가 0이 됩니다. 이런 경우에는 겹치는 것으로 판단할 수 있습니다. 만약 네 점이 모두 일직선상에 놓이는 경우에는 모든 CCW가 0이 됩니다. 이런 경우에는 두 선분이 겹치는지 좌표를 통해 판단해야 합니다.

다만 주의해야 할 점으로 CCW를 곱할 때 long long 범위를 벗어날 수 있기 때문에 -1, 0, 1로 변환해서 곱해주는 것이 좋습니다.
#include <bits/stdc++.h>
using namespace std;
void fio() { cin.tie(nullptr); cout.tie(nullptr); ios::sync_with_stdio(false); }
using ll = long long;
using pll = pair<ll,ll>;
ll ccw(pll a, pll b, pll c) {
ll tmp = (b.first - a.first) * (c.second - a.second)
- (b.second - a.second) * (c.first - a.first);
if (tmp > 0) return 1;
if (tmp < 0) return -1;
return 0;
}
bool isIntersect(pll a, pll b, pll c, pll d) {
ll ab = ccw(a, b, c) * ccw(a, b, d);
ll cd = ccw(c, d, a) * ccw(c, d, b);
if (ab == 0 && cd == 0) {
if (a > b) swap(a, b);
if (c > d) swap(c, d);
return c <= b && a <= d;
}
return ab <= 0 && cd <= 0;
}
int main() {
fio();
pll a, b, c, d;
cin >> a.first >> a.second >> b.first >> b.second;
cin >> c.first >> c.second >> d.first >> d.second;
cout << isIntersect(a, b, c, d) << '\n';
}