[기하] ccw, 선분교차

Jewook·2022년 7월 21일

알고리즘

목록 보기
2/14

간결한 기하 코드를 위한 벡터 클래스

실제로는 문제에 맞게 필요한 부분만 구현하거나, 변형해서 사용한다.
아래서 작성할 코드들을 간결하게 하기 위해 필요한 기능을 대부분 구현해놓았다. (종만북)
2차원 벡터이며, 잘 이해하면 n차원으로 그대로 확장이 가능하다.

const double PI = acos(-1.0);
struct Vector2{
   
    double x, y;

    explicit Vector2(double x_=0, double y_=0) : 
        x(x_), y(y_) {}

    bool operator ==(const Vector2& b) const {
        return x == b.x && y == b.y;
    }
	// x,y순서대로 정렬
    bool operator <(const Vector2& b) const {
        return x != b.x ? x < b.x : y < b.y;
    }

    Vector2 operator + (const Vector2 & b) const {
        return Vector2(x + b.x, y + b.y);
    }

    Vector2 operator - (const Vector2& b) const {
        return Vector2(x - b.x, y - b.y);
    }

    Vector2 operator *(double b) const {
        return Vector2(b * x, b * y);
    }
	// 벡터의 크기(길이)라고 생각하면 된다. 
    inline double norm() const {
        return hypot(x, y);
    }
	// 정규화
    Vector2 normalize() const {
        return Vector2(x / norm(), y / norm());
    }
	// x기준으로 시계방향 각도 
    double polar() const {
        return fmod(atan2(y, x) + 2 * PI, 2 * PI);
    }
	// 내적
    double dot(const Vector2& b) const {
        return x * b.x + y * b.y;
    }
	//외적
    double cross(const Vector2& b) const {
        return x * b.y - y * b.x;
    }
	//정사영
    Vector2 projection(const Vector2& b) const {
        Vector2 normed = b.normalize();
        return normed * normed.dot(*this);
    }
};

ccw(Counter Clock Wise)

// 방향만 판단하려면 부호만 리턴할 수 있다.
// 값은 면적 계산에 사용할 수 있다.
double ccw(Vector2 a, Vector2 b) {
	return a.cross(b);
}

// a->b->c로 이어지는 선분의 방향
double ccw(Vector2 a, Vector2 b, Vector2 c) {
	return ccw(b-a, c-a);
}

외적의 성질을 이용해서, 두 선분(벡터)가 이루는 각의 방향을 판단하는 알고리즘이다.
양수면 a를 기준으로 b와이루는 각도가 반시계방향, 0이면 한 직선에 포함, 음수이면 시계방향이다.

직선 교차

먼저 교차의 정의를 명확히 하자. 엄밀한 수학정인 정의는 알지 못한다. 프로그래밍 문제 해결의 측면에서, 선분의 일부가 겹쳐도(즉, 유일한 교점을 정의할 수 없어도) 교차하는 것으로 하자.

bool lineIntersection(Vector2 a1, Vector2 a2, Vector2 b1, Vector b2) {
	//실수 오차 고려
	if ( fabs((a2-a1).cross(b2-b1)) <= EPSILON)
    	return true;
    else
    	return false;
}

직선은 무한히 이어진 선분이기 때문에, 평행하지만 않다면 분명 교차한다.

교점을 직접 구하고 싶다면 좀 복잡해진다. 두 직선이 평행하다면 두 직선이 완전히 일치하는 경우도 존재할 수 있지만 false를 반환하도록 하겠다. 즉, 교점이 유일한경우만 true를 반환하고 교점을 res에 넣는다.

bool lineIntersection(Vector2 a1, Vector2 a2, 
Vector2 b1, Vector b2, Vector& res) {
	Vector2 a = a2-a1;
    Vector2 b = b2-b1;
	double det = (a).cross(b);
	if (fabs(det) <= EPSILON)
    	return false;
        
    // a1 + a * p
    res = a1 + a*(b1.cross(b) - a1.cross(b)) / det;
    return true;
}

아이디어는 다음과 같다. a1~a2로 이루어진 직선을 a, 마찬가지로 b1~b2로 이루어진 직선 b이다.
a = a2-a1(직선 a의 방향벡터), b=(b2-b1)(직선 b의 방향벡터)라고 하면, p와 q에 대한 연립방정식을 풀면 교점을 구할 수 있다는 것을 알 수 있다. (평행이 아닌 경우에만)

a1 + p*a = b1 + q*b
-> a1.x + p*a.x = b1.x + q*b.x
-> a1.y + p*a.y = b1.y + q*b.y

연립방정식을 잘 풀면,

p = (b1*b - a1*b) / a*b 이다.
(이미 a*b ==0이면 평행하다는 것을 보였다. 평행한 경우를 제외하고는 교점이 무조건 정의된다는 것을 알 수 있다.)

p 를 a1 + p*a에 대입하면, 교점을 알 수 있다.

선분의 교차

선분의 교차는 더 복잡하다.
1) 한 직선에 있지 않음(비교차, 서로 중간에서 교차, 한선분의 끝과 교차)
2) 두 선분이 한 직선에 있음(안겹침, 한점겹침, 부분겹침, 포함)
등등 고려해야할 케이스가 많다.

bool segmentIntersects(Vector2 a1, Vector2 a2, Vector2 b1, Vector2 b2) {
    double ccw1 = ccw(a1, a2, b1) * ccw(a1, a2, b2);
    double ccw2 = ccw(b1, b2, a1) * ccw(b1, b2, a2);
    // 두 선분이 한직선
    if (ccw1 == 0 && ccw2 == 0) {
        // a1<a2 and b1<b2
        if (a2 < a1) swap(a1, a2);
        if (b2 < b1) swap(b1, b2);
        return !(a2 < b1 || b2 < a1);
    }
    // 평행하지 않은 case(둘다 0아님) + 한점에서 만나는 케이스 (둘중 하나만 0)
    else
        return ccw1 <= 0.0 && ccw2 <= 0.0;
}

ccw를 활용해서 해결할 수 있다.

직점 교점을 구하고 싶은 경우는 우선, 평행한 경우와 평행하지 않은 경우로 나누어 해결할 수 있다.

1) 평행하지 않다면, 직선의 교점 구하는 알고리즘을 활용해 구한 교점이 각 선분에 포함되는지 판단하면 된다.
2) 평행하다면, 두 선분이 서로 겹치는지 판단하고 겹치면 교선분 중의 임의의 점을 선택하면 된다.

bool segmentIntersects(Vector2 a1, Vector2 a2, Vector2 b1, Vector2 b2, Vector2& res) {
	// WLOG, a1<a2, b1<b2
    if(a2<a1) swap(a1,a2);
    if(b2<b1) swap(b1,b2);

    // 평행한 경우, 
    if (!lineIntersection(a1,a2,b1,b2,res)) {
        // 평행하지만 한직선에 있지 않은경우 + 
        // 한직선에 있어도 서로 겹치지 않는경우 
        if (ccw(a1,a2,b1)!=0 || a2<b1 || b2<a1)
        	return false;
        // 최소 한점 이상에서 겹치는게 확실하다.
        // 잘 선택해서 res에넣고 true를 리턴하자.
        if (a1<b1) res = b1; else res = a1;
        return true;
    }
    // 평행하지 않은 경우
    // lineIntersection의 결과로, 선분을 직선으로 연장했을 때의 교점이
    // 현재 res에 들어있다. 각 선분에 포함되는지 보자.

   	if (res<a1 || a2<res || res<b1 || b2<res)
        return false;
    return true;

}

점과 선의 거리

내적과 정사영의 개념을 알면 쉽게 해결할 수 있다.
a1, a2가 이루는 직선에 b가 내린 수선의 발을 구하는 코드이다.

Vector2 perpendicularFoot(Vector2 a1, Vector2 a2, Vector2 b) {
	return a1 + (b-a1).projection(a2-a1);
}

수선의 발과 b와의 거리를 구하면 된다.

double pointToLine(Vector2 a1, Vector2 a2, Vector2 b) {
	return (b-perpendicularFoot(a1,a2,b)).norm();
}
profile
https://solved.ac/profile/huh0918

0개의 댓글