FOSSIL (상, p. 486) - 다시 풀어봄.

문제#

image.png


기하로 풀어보기#

두 볼록 다각형(Convex Hull)이 겹치는 구간도 볼록 다각형이다. 이 새로운 볼록 다각형에서 남북 방향의 최대 길이를 구하면 되는데, 남북 방향의 최대 길이는 새로운 볼록 다각형의 꼭지점에서만 생성되므로, 각 꼭지점 에서의 길이중 최대 값을 출력하면 된다.

상세 방법은 아래와 같다.

1. 교집합 볼록 다각형(Convex Hull)을 구한다.#

1) 두 볼록 다각형을 점이 아닌 선분으로 나타낸다.#

선분으로 따로 나타내면 반복문 돌리기가 쉽다.

2) 두 볼록 다각형의 교점을 구한다.#

두 선분 집합을 이중 for문으로 돌려서 교점이 있는지 확인하고, 교점이 있으면 해당 교점을 구한다. 교점은 벡터의 외적을 활용하여 구한다.

① 먼저 교차점의 존재 여부부터 파악해야 한다.

2차원에서 두 벡터 $\vec{u}=(x_1,y_1),\; \vec{v}=(x_2,y_2)$의 외적은 다음과 같은 스칼라 값으로 정의한다.

$$ \vec{u} \times \vec{v} = x_1y_2 - y_1x_2 $$

그리고 이 결과값의 부호가 방향을 의미한다.

  • > 0: 반시계(CCW)
  • < 0: 시계(CW)
  • = 0: 같은 직선(colinear)

참고로 이 결과값의 크기(절대값)는 두 벡터가 이루는 평행사변형의 넓이를 의미한다.

아래 그림을 보면, $\overline{AB}$와 $\overline{CD}$는 교차한다.

그러면 $\overrightarrow{AB} \times \overrightarrow{AC} \;$와 $\overrightarrow{AB} \times \overrightarrow{AD} \;$는 부호가 다를 수밖에 없다. $\left((\overrightarrow{AB} \times \overrightarrow{AD})\cdot (\overrightarrow{AB} \times \overrightarrow{AD}) \le 0\right)$

image1.png

그런데 이 것만 고려하면, 두 선분 $\overline{AB}$와 $\overline{CD}$가 만나지 않는 경우를 제대로 고려할 수 없다. 교차점 존재 여부를 파악하기 위해선 $(\overrightarrow{CD} \times \overrightarrow{CA})\cdot (\overrightarrow{CD} \times \overrightarrow{CB}) \le 0$도 만족하는지 봐야한다.

② 교차점 존재 여부를 파악했으면, 교차점($I$) 을 구한다.

$\overline{AB}$위의 모든 점은 아래와 같이 벡터와 파라미터를 사용해서 나타낼 수 있다.

$$ P(t) = A + t(B-A),\quad 0\le t \le 1 $$

$\overline{CD}$도 마찬가지로 아래와 같이 나타낼 수 있다.

$$ Q(u) = C + w(D-C), \quad 0 \le u \le 1 $$

그러므로 교차점 $I$는 아래의 식을 풀면 된다.

$$ A + t(B-A) = C+u(D-C) $$$$ \Rightarrow t(B-A) = (C-A) + u(D-C) $$

양변에 $(D-C)$로 외적을 취한다.

$$ \Rightarrow t(B-A)\times (D-C) = (C-A)\times(D-C) $$

왜냐면, $u(D-C) \times (D-C) = 0$ 이기 때문이다. (자기 자신과의 외적은 항상 0)

$$ \therefore t = \frac{(C-A)\times(D-C)}{(B-A)\times (D-C)} $$

여기서 분모가 0이 된다는 것은 $\overline{AB} \parallel \overline{CD}$이거나 두 선분이 한 직선위에 있는 상황인데, 두 선분이 한 직선위에 있는 상황은 문제에서 주어지지 않는다 하였고(”한 직선 위에 있는 세 점이 주어지는 일도 없습니다.”), 위의 방식으로 교차점을 구하기 전에 교차점이 있는지 이미 확인한 상태이므로 분모가 0이 되는 상황은 발생하지 않는다.

이제 위의 t값을 P(t)에 대입하면 교차점 $I$를 구할 수 있다.

3) 한쪽 볼록 다각형이 포함되는 나머지 볼록 다각형의 정점들을 구한다.#

이제 교집합 볼록 다각형을 이루는 나머지 점들을 구해야 하는데, 이러한 점들은 볼록 다각형에 포함되는 다른 볼록 다각형의 정점들이다. 그래서 볼록 다각형 V의 정점들이 다른 볼록 다각형인 W안에 포함되는지를 확인하고, 그 반대로도 포함여부를 확인해주면 된다.

정점 $p$가 볼록 다각형 $V$에 포함되는지 확인하는 방법은 아래와 같다.

모든 $i$에 대해 점 $p$로 부터 항상 $(v[i\text{+}1]-v[i]) \times (p -v[i]) \le 0$ 이면, 정점 p가 볼록 다각형 V에 포함된다.

코드로 구현하면 아래와 같다.

void SortCvhByCcwOrder(vector<Vec>& u) {
    //모든 점의 중심점을 계산한다.
    double midx, midy;
    Vec mid(0.0, 0.0);
    for (int i = 0; i < u.size(); i++) {
        mid = mid + u[i];
    }
    mid = mid * (1.0/u.size());

    //중심점과 Convex Hull의 점점들 간의 PolarAngle값을 구하고, 그 값 순서로 정렬한다.
    auto cmp = [mid](const Vec& lhs, const Vec& rhs) {
        double lang = (lhs - mid).PolarAngle();
        double rang = (rhs - mid).PolarAngle();
        if (fabs(lang - rang) < ES) {
            return (lhs - mid).Len() < (rhs - mid).Len();
        }
        else return lang < rang;
        };

    sort(u.begin(), u.end(), cmp);
}

2. 교집합 볼록 다각형의 남북방향 최대 길이를 계산한다.#

1) 교집합 볼록 다각형의 정점개수가 1이하면, 남북방향 최대 길이는 0이다.#

2) 교집합 볼록 다각형을 CCW(시계 반대 방향) 방향으로 정렬한다.#

남북방향 최대 길이를 쉽게 계산하기 위해, 교집합 볼록 다각형 U를 윗껍질과 아랫껍질로 나누려고 한다. 이렇게 나누게 되면 x좌표에 해당하는 윗껍질의 y좌표와 아랫껍질의 y좌표값을 뺀 것이 좌표 x에서의 남북방향 길이가 된다.

이렇게 윗껍질과 아랫껍질로 나누려면, 볼록 다각형의 정점들이 CCW 방향으로 정렬되어 있어야 한다. 정렬 방법은 볼록 다각형 정점들의 평균이 되는 중앙점(mid)을 구하고, 이 중앙점과 볼록 다각형의 모든 정점간의 각도 순으로 정렬하면 된다. (각도는 x축부터 반시계방향으로 $[0,2\pi)$값을 갖는다.)

x=a꼴의 선분의 경우 윗껍질/아랫껍질 어느 것에도 포함시키지 않는다. 이런 경우 어차피 x=a와 맞닿아 있는 선분들을 고려하면 x=a에 해당하는 남북방향 길이가 구해지기 때문이다.

3) 교집합 볼록 다각형의 정점마다 남북방향의 길이를 측정하고 최댓값을 구한다.#

윗껍질 함수: $y = f(x)$ 아랫껍질 함수: $y = g(x)$

라고 할 때, $max(f(x)-g(x))$를 구하면 된다.


정답 코드 (기하)#

#include <bits/stdc++.h>
using namespace std;
constexpr double ES = 1e-8; //Epsilon
constexpr double NA = 105.0; //Not Available

inline double PI() {
    static const double pi = acos(-1.0);
    return pi;
}
inline int Sign(double x) {
    return (x>-ES)-(x<ES);
}
struct Vec {
    double x,y;
    Vec() {}
    Vec(double x, double y):x(x),y(y) {}
    bool operator<(const Vec & rhs) const {
        return (x!=rhs.x ? x<rhs.x : y<rhs.y);
    }
    bool operator==(const Vec & rhs) const {
        return x==rhs.x && y==rhs.y;
    }
    bool operator<=(const Vec & rhs) const {
        return (*this < rhs || *this == rhs);
    }
    Vec operator*(double k) const {
        return Vec(k*x, k*y);
    }
    Vec operator-(const Vec & rhs) const {
        return Vec(x-rhs.x,y-rhs.y);
    }
    Vec operator+(const Vec & rhs) const {
        return Vec(x+rhs.x,y+rhs.y);
    }
    double PolarAngle() const {
        return fmod(atan2(y,x)+2*PI(), 2*PI());
    }
    double Dot(const Vec & rhs) const {
        return x*rhs.x + y*rhs.y;
    }
    double Cross(const Vec & rhs) const {
        return x*rhs.y - rhs.x*y;
    }
    double Len() const {
        return hypot(x,y);
    }
    Vec UnitVec() const {
        return Vec(x/Len(), y/Len());
    }
    Vec ProjectTo(const Vec & rhs) const {
        Vec uv = rhs.UnitVec();
        return uv*(this->Dot(uv));
    }
    int Ccw(const Vec & rhs) const {
        //return값은 아래와 같다.
        // 1) rhs가 *this의 CCW방향 위치에 있을 때: 1
        // 2) rhs가 *this의 CW방향 위치에 있을 때: -1
        // 3) rhs가 *this와 방향이 동일하거나 180도 정반대 방향일 때: 0

        return Sign(this->Cross(rhs));
    }
    void Print() const {
        printf("(%.3lf, %.3lf)\n", x,y);
    }
};
struct LineSeg { //선분
    Vec l,r;
    LineSeg() {}
    LineSeg(const Vec & lhs, const Vec & rhs) {
        if(rhs < lhs) l=rhs, r=lhs;
        else l=lhs, r=rhs;
    }
    bool HasIntersection(const LineSeg & rhs, Vec & intersection) const {
        Vec p = r-l;
        Vec a = rhs.r-l;
        Vec b = rhs.l-l;

        Vec q = rhs.r-rhs.l;
        Vec c = r-rhs.l;
        Vec d = l-rhs.l;

        if(p.Ccw(a)*p.Ccw(b)==0 && q.Ccw(c)*q.Ccw(d)==0) { //두 선분이 한 직선위에 있는 경우
            if(r<rhs.l || rhs.r<l) return false;
            else {
                //겹치는 부분이 최소 한 점 이상인 경우, 교차점을 아무거나 return
                if(rhs.l <= r) intersection = rhs.l;
                else intersection = r;
                return true;
            }
        }
        else {
            if(p.Ccw(a)*p.Ccw(b)<=0 && q.Ccw(c)*q.Ccw(d)<=0) {
                double t = (rhs.l-l).Cross(rhs.r-rhs.l) / (r-l).Cross(rhs.r-rhs.l);
                intersection = l + ((r-l)*t);
                return true; //p x a(외적)와 p x b의 부호가 다르면 교차점이 존재한다.
            }
            else return false;
        }
    }
    bool IsBetweenX(double x) const {
        double mn = min(l.x, r.x);
        double mx = max(l.x, r.x);
        return mn <= x && x <= mx;
    }

    //*this 선분에서 x좌표값에 해당하는 y좌표(f(x)) 값을 return한다.
    double F(double x) const {
        assert(r.x != l.x);

        if(IsBetweenX(x)) {
            double y = (r.y - l.y) / (r.x - l.x) * (x - l.x) + l.y;
            if (l.y <= r.y) {
                if (l.y <= y && y <= r.y) return y;
            }
            else {
                if (r.y <= y && y <= l.y) return y;
            }
        }
        return NA;
    }
};

double F(double x, vector<LineSeg> & ls) {
    for(int i=0; i<ls.size(); i++) {
        if(ls[i].r.x == ls[i].l.x) continue;

        double y = ls[i].F(x);
        if(y >= NA) continue;
        return y;
    }
    return NA;
}

//정점p가 Ccw로 정렬된 Convex Hull 안에 포함되는지를 본다.
//<=0으로 비교했기 때문에 Convex Hull 경계에 있어도 true를 리턴한다.
bool IsIn(Vec & p, vector<Vec> & ccw_cvh) {
    for(int i=1; i<ccw_cvh.size(); i++) {
        if((ccw_cvh[i]-ccw_cvh[i-1]).Ccw(p-ccw_cvh[i-1]) <= 0) return false;
    }
    return true;
}

void SortCvhByCcwOrder(vector<Vec>& u) {
    //모든 점의 중심점을 계산한다.
    double midx, midy;
    Vec mid(0.0, 0.0);
    for (int i = 0; i < u.size(); i++) {
        mid = mid + u[i];
    }
    mid = mid * (1.0/u.size());

    //중심점과 Convex Hull의 점점들 간의 PolarAngle값을 구하고, 그 값 순서로 정렬한다.
    auto cmp = [mid](const Vec& lhs, const Vec& rhs) {
        double lang = (lhs - mid).PolarAngle();
        double rang = (rhs - mid).PolarAngle();
        if (fabs(lang - rang) < ES) {
            return (lhs - mid).Len() < (rhs - mid).Len();
        }
        else return lang < rang;
        };

    sort(u.begin(), u.end(), cmp);
}

vector<Vec> v,w,u;
vector<LineSeg> ls_v, ls_w;
vector<LineSeg> lower_ls, upper_ls;
int main() {
    ios::sync_with_stdio(false); cin.tie(0);
    int tc; cin >> tc;
    while(tc--) {
        v.clear(); w.clear();
        ls_v.clear(); ls_w.clear();
        int n,m; cin >> n >> m;

        double x,y;
        for(int i=0; i<n; i++) {
            cin >> x >> y;
            v.emplace_back(x,y);
        }
        v.emplace_back(v[0]);

        for(int i=0; i<m; i++) {
            cin >> x >> y;
            w.emplace_back(x,y);
        }
        w.emplace_back(w[0]);

        //Convex Hull을 선분으로 다시 표현.
        for(int i=0; i<n; i++) {
            ls_v.emplace_back(v[i],v[i+1]);
        }
        for(int i=0; i<m; i++) {
            ls_w.emplace_back(w[i],w[i+1]);
        }

        //u: 새로운 Convex Hull 정점들
        u.clear();
        Vec intsc;
        //1) v와 w의 교점을 u에 넣는다.
        for(int i=0; i<n; i++) {
            for(int j=0; j<m; j++) {
                if(ls_v[i].HasIntersection(ls_w[j], intsc)) {
                    u.emplace_back(intsc);
                }
            }
        }

        //2) v의 정점이 w Convex Hull에 속하면 u에 넣는다.
        //   w의 정점이 v Convex Hull에 속하면 u에 넣는다.
        for(int i=0; i<n; i++) {
            if(IsIn(v[i], w)) u.emplace_back(v[i]);
        }
        for(int j=0; j<m; j++) {
            if(IsIn(w[j], v)) u.emplace_back(w[j]);
        }

        //u의 정점개수가 1개이하면, 문제의 답은 항상 0이다.
        if(u.size()<=1) {
            printf("%.8lf\n", 0.0);
            continue;
        }

        //u를 ccw 방향 순으로 정렬한다.
        SortCvhByCcwOrder(u);
        u.emplace_back(u[0]);

        lower_ls.clear();
        upper_ls.clear();
        for(int i=0; i<u.size()-1; i++) {
            if(u[i+1].x > u[i].x) lower_ls.emplace_back(u[i], u[i+1]);
            else if(u[i+1].x < u[i].x) upper_ls.emplace_back(u[i+1], u[i]);
        }

        double ans = 0.0;
        for(int i=0; i<u.size()-1; i++) {
            ans = max(ans, F(u[i].x, upper_ls) - F(u[i].x, lower_ls));
        }

        printf("%.8lf\n", ans);
    }
    return 0;
}

주의할 점#

아래와 같은 경우도 고려되었는지 확인해야 한다.

image2.png

좌측의 그림은 두 Convex Hull간의 겹치는 부분에 두 Convex Hull의 꼭지점이 하나도 포함되어 있지 않은 경우도 있다는 것을 말한다.

우측의 그림은 한 Convex Hull이 다른 Convex Hull을 완전히 포함하는 경우를 말한다.


추가 Test Case와 Output#

//Input
2
3 3
0 0 6 2 0 4
2 2 4 0 4 4
4 4
20 20 80 20 80 80 20 80
40 40 60 40 60 60 40 60

//Output
2.00000000
20.00000000

Comment#

기하 문제 푸는 방식에 대해 잘 모르고 풀었다가 책을 보고 삼분 탐색으로 풀어본 뒤 나중에 계산 기하 챕터를 공부하며 다시 풀어보게 되었다.


책 풀이 (삼분 탐색)#

이 문제는 수치 해석파트에 있는 문제다. 벡터 없이도 풀 수 있고, 책에서는 삼분 탐색으로 풀고 있다. 풀이 방법은 아래와 같다.

두 볼록 다각형(Convex Hull)의 윗 껍질과 아랫 껍질을 나누어 저장하는데, 어느 볼록 다각형의 껍질인지 구분할 필요가 없이 저장하고, 두 껍질에서의 x값에 따른 y값 차이의 최소를 삼분탐색으로 최대가 되는 지점을 찾는 것이다.


정답 코드 (삼분 탐색)#

#include <bits/stdc++.h>
using namespace std;
using pdd = pair<double, double>;
using ppddpdd = pair<pdd, pdd>;
vector<pdd> v, w;
vector<ppddpdd> upper, lower;
bool IsBetween(double x1, double x2, double x) {
    return (x1 <= x && x <= x2) || (x2 <= x && x <= x1);
}
double At(ppddpdd& line, double x) {
    double a = (line.second.second - line.first.second) / (line.second.first - line.first.first);
    return a * (x - line.first.first) + line.first.second;
}
double GetUpperY(vector<ppddpdd>& upper, double x) {
    double mn_y = 100;
    for (int i = 0; i < upper.size(); i++) {
        if (IsBetween(upper[i].first.first, upper[i].second.first, x)) {
            mn_y = min(mn_y, At(upper[i], x));
        }
    }
    return mn_y;
}
double GetLowerY(vector<ppddpdd>& lower, double x) {
    double mx_y = 0;
    for (int i = 0; i < lower.size(); i++) {
        if (IsBetween(lower[i].first.first, lower[i].second.first, x)) {
            mx_y = max(mx_y, At(lower[i], x));
        }
    }
    return mx_y;
}
double GetMinX(vector<pdd>& g) {
    double mn = 100;
    for (int i = 0; i < g.size() - 1; i++) {
        mn = min(mn, g[i].first);
    }
    return mn;
}
double GetMaxX(vector<pdd>& g) {
    double mx = 0;
    for (int i = 0; i < g.size() - 1; i++) {
        mx = max(mx, g[i].first);
    }
    return mx;
}
int main() {
    ios::sync_with_stdio(false); cin.tie(0);
    int tc; cin >> tc;
    while (tc--) {
        int n, m;
        cin >> n >> m;

        v.clear(); w.clear();
        lower.clear(); upper.clear();
        double x, y;
        for (int i = 0; i < n; i++) {
            cin >> x >> y;
            v.emplace_back(x, y);
        }
        for (int i = 0; i < m; i++) {
            cin >> x >> y;
            w.emplace_back(x, y);
        }

        v.push_back(v[0]);
        w.push_back(w[0]);
        for (int i = 1; i < v.size(); i++) {
            if (v[i].first > v[i - 1].first) lower.emplace_back(v[i - 1], v[i]);
            else if (v[i].first < v[i - 1].first) upper.emplace_back(v[i], v[i - 1]);
        }
        for (int i = 1; i < w.size(); i++) {
            if (w[i].first > w[i - 1].first) lower.emplace_back(w[i - 1], w[i]);
            else if (w[i].first < w[i - 1].first) upper.emplace_back(w[i], w[i - 1]);
        }

        sort(lower.begin(), lower.end());
        sort(upper.begin(), upper.end());

        double lo = max(GetMinX(v), GetMinX(w));
        double hi = min(GetMaxX(v), GetMaxX(w));
        for (int z = 0; z < 100; z++) {
            double lmid = (lo * 2 + hi) / 3;
            double rmid = (lo + hi * 2) / 3;
            double lres = GetUpperY(upper, lmid) - GetLowerY(lower, lmid);
            double rres = GetUpperY(upper, rmid) - GetLowerY(lower, rmid);
            if (lres < rres) lo = lmid;
            else hi = rmid;
        }

        double ans_x = lo;
        double ans_len = GetUpperY(upper, ans_x) - GetLowerY(lower, ans_x);
        if (ans_len < 0) ans_len = 0.0;
        printf("%.8lf\n", ans_len);
    }
    return 0;
}