Algorithms

[백준 #11758 c++] CCW

hyunbae 2023. 11. 19. 13:51

https://www.acmicpc.net/problem/11758

 

11758번: CCW

첫째 줄에 P1의 (x1, y1), 둘째 줄에 P2의 (x2, y2), 셋째 줄에 P3의 (x3, y3)가 주어진다. (-10,000 ≤ x1, y1, x2, y2, x3, y3 ≤ 10,000) 모든 좌표는 정수이다. P1, P2, P3의 좌표는 서로 다르다.

www.acmicpc.net

 

겉보기에는 단순한 문제이다.

세 점이 이루는 두 선분의 방향이 시계인지 반시계인지를 알아내는 것은 딱 생각했을 때 다음과 같다.

 

점 A, B, C를

A(x1,  y1), B(x2, y2), C(x3, y3) 라고 정의할 때,

 

점AB가 이루는 직선상에 점C가 있는 경우 0을 출력하면 된다.

그리고 직선상에 있지 않은 경우에는 둘로 나눠지는데,

 

점A보다 점B가 오른쪽에 있는 경우 우상향 또는 우하향 직선이 되고 즉,

x1 < x2 인 경우,

점C가 직선AB보다 위에 있을 때에는 반시계 방향이 된다.

 

반면 점B가 점A의 왼쪽에 있는 경우 좌상향 또는 좌하향 직선이 되고 즉,

x1 > x2 인 경우,

점C가 직선AB보다 위에 있을 때에는 시계방향이 된다.

 

그냥 이렇게 푼 결과를 코드로 나타내면 다음과 같다.

 

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <algorithm>
#include <vector>
#include <string>
#include <cmath>
using namespace std;

struct Coordinate {
    int x;
    int y;
};

struct Linear {
    double gradient;
    double yIntercept;
};

int getPosY(Linear func, Coordinate coord) {
    double fy = coord.x * func.gradient + func.yIntercept;
    if (coord.y > fy) return 1;
    else if (coord.y == fy) return 0;
    else return -1;
}

Linear getLinear(Coordinate c1, Coordinate c2) {
    Linear lin;
    lin.gradient = (double)(c2.y - c1.y) / (c2.x - c1.x);
    lin.yIntercept = c1.y - lin.gradient * c1.x;
    return lin;
}

int main() {
    ios::sync_with_stdio(false); cin.tie(NULL); cout.tie(NULL);
    
    vector<Coordinate> v;
    int n = 3;
    while (n--) {
        Coordinate coord;
        cin >> coord.x >> coord.y;
        v.push_back(coord);
    }

    Linear lin = getLinear(v[0], v[1]);
    int result = getPosY(lin, v[2]);
    if (!result) cout << 0;
    else {
        if (v[1].x - v[0].x > 0) {
            cout << result;
        }
        else {
            cout << result * -1;
        }
    }
    
    return 0;
}

 

하지만 이 코드는 컴퓨터가 실수를 저장하는 방법을 간과했다.

컴퓨터는 부동소수점으로 실수를 저장하기 때문에 0.1 같은 숫자를 완벽하게 표현하지 못한다.

즉, 점C가 직선상에 있어야 하지만 간발의 차이로 직선보다 위에 있다고 계산해버릴 수 있다.

 

그래서 이 문제는 직접 풀기보다는 CCW 알고리즘을 한 번 검색해서 풀어보는 것을 권하는 것이다.


CCW: Counter Clockwise

평면위에 찍힌 세 점이 이루는 선분의 방향을 찾는 알고리즘이다.

하지만 위의 방법과 달리 부동소수점이 아닌 정수형으로만 계산하기 때문에 오차가 발생하지 않는다.

 

그리고 이 알고리즘을 이해하기 위해서는 선형대수학, 그 중에서도 벡터의 외적의 개념을 알아야 한다.

 

다시 중고등학교 시절로 돌아가보자.

벡터의 외적의 정의는 3x3행렬의 곱이며 그 크기가 두 벡터가 이루는 평행사변형의 넓이와 같다고도 하지만,

더 쉽게 설명하면 3차원 공간상에서 두 벡터와 수직인 법선 벡터를 말한다.

 

이와 관련한 정보는 다른 곳에 찾으면 다 나오고 여기서는 문제와 관련한 외적 공식만 적용한다.

 

벡터의 외적은 기본적으로 3차원 공간상의 두 벡터를 다루기 때문에 이를 2차원 평면으로 변환해야 한다.

이 때 외적 공식을 보면,

 

두 벡터 A(a1, a2, a3), B(b1, b2, b3) 가 이루는 각을 θ, 법선 벡터를 N이라 할 때 외적 A X B(A cross B)는

A X B = (|A||B|sinθ)N = (a2b3 - a3b2, a3b1 - a1b3, a1b2 - a2b1)

으로 정의된다. (|A|는 벡터A의 크기를 나타낸다.)

 

여기서 우리는 2차원 평면상의 두 벡터를 다루기 때문에 a3 = 0, b3 = 0 이므로

A X B = (0, 0, a1b2 - a2b1)

로 x, y값을 모두 0으로 만들어 버릴 수 있다.

그리고 이 결과는 딱 봐도 평면과 수직인 법선 벡터임을 알 수 있다.

(여기서 a1b2 - a2b1은 2x2 행렬의 역행렬 공식에 있는 ad-bc 로 이해하면 외우기 쉽다.)


 

벡터A = 점A에서 점B로 가는 벡터 = 점B - 점A

벡터B = 점A에서 점C로 가는 벡터 = 점C - 점A

라고 해보자.

 

벡터A(a1, a2), 벡터B(b1, b2)에 대해

a1b2 - a2b1을 각 점의 좌표값의 식으로 전개하면

각 점의 좌표를 (x1, y1), (x2, y2), (x3, y3) 라고 할 때

a1 = x2 - x1, a2 = y2 - y1

b1 = x3 - x1, b2 = y3 - y1 이므로

a1b2 - a2b1 = (x2 - x1)(y3 - y1) - (y2 - y1)(x3 - x1) 이 된다.

 

우리는 a1b2 - a2b1의 값을 놓고

양수라면 (오른손 법칙에 의해) 반시계 방향이 되고

음수라면 시계 방향이 됨을 알 수 있다.

 

0이라면 직선상에 놓였다고 볼 수 있는데, 그 이유는

위의 벡터의 정의에서 직선상에 놓인 경우는 두 벡터가 이루는 각이 0 또는 180도가 된다는 의미이고

이는 외적의 정의에 의해서 외적의 결과가 sin0 = sinπ = 0, 즉 크기가 0인 벡터가 되기 때문이다.

 

위 내용을 코드로 만들면

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <algorithm>
#include <vector>
#include <string>
#include <cmath>
using namespace std;

struct Coordinate {
    int x;
    int y;
};

int ccw(vector<Coordinate>& cv) {
    return (cv[1].x - cv[0].x) * (cv[2].y - cv[0].y) - (cv[2].x - cv[0].x) * (cv[1].y - cv[0].y); // 벡터 AB와 벡터 BC를 외적했을 때의 z값. 벡터 AB는 점B - 점A로 정의된다.
}

int main() {
    ios::sync_with_stdio(false); cin.tie(NULL); cout.tie(NULL);
    
    vector<Coordinate> v;
    int n = 3;
    while (n--) {
        Coordinate coord;
        cin >> coord.x >> coord.y;
        v.push_back(coord);
    }

    int result = ccw(v);
    if (result < 0) cout << -1;
    else cout << (result > 0);
    
    return 0;
}