볼록 껍질 알고리즘

플레티넘 이상의 문제에서 그리디 알고리즘이나 애드혹을 제외한 알고리즘들은 알고리즘을 구현하는 방법을 알고 있어도, 그 구현 자체와 개념 이해에 난이도가 붙는 방식이다.

즉 알고리즘을 알고 있을 때, 난이도가 플레티넘이라는 것이다.

플레티넘 이상 알고리즘 태그가 붙은 문제는 공부를 해야 풀 수 있다. 혼자서 푼다면, 대학원 가는 것을 추천한다.

그중 플레문제를 풀고 싶은 당신께 볼록껍질 알고리즘에 대해 소개해보자 한다.

정확히는 Graham Scan 알고리즘이다.

볼록 껍질 알고리즘이란,

좌표평면 상에 점의 집합이 구해졌을 때, 바깥 쪽 점들을 구하는 알고리즘이다.

fawefawef.png

이와 같이 나머지 점이 다 안쪽에 있는 다각형을 만드는 것이다.

내가 여기서 쓴 알고리즘은 꼭짓점을 구하는 알고리즘이다.

이해도가 높다면 개량하여, 모든 점을 구해보는 것도 시도해보는 것을 추천한다.


class Point {

    int x, y;

    Point (int x, int y) {

        this.x = x;

        this.y = y;

    }

}

간단하게 CCW 를 구현 하여 보겠다.


public static int CCW (Point a, Point b, Point c) {

        int g = (c.x - a.x) * (b.y - a.y) - (b.x - a.x) * (c.y - a.y);

        return Integer.compare(g, 0);

    }

반시계 일때 -1, 시계 일때 1, 평행할 때 0 을 리턴한다.

점들을 정렬하여, 맨 왼쪽 아래 점을 찾자. 그 점(central)은 볼록다각형에 무조건 포함된다.

정렬을 해보겠다.


Arrays.sort(points, new Comparator<Point>() {

            @Override

            public int compare(Point o1, Point o2) {

                if (o1.y == o2.y) return Integer.compare(o1.x, o2.x);

                return Integer.compare(o1.y, o2.y);

            }

        });

Comparator 를 모른다면, 이 문제를 풀 수 없다.

풀 수 없다.

이 문제를 풀고 싶지만, Comparator가 뭔지 모른다면,

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

이문제를 Comparator를 이용하여 풀고 오면 된다.

자 그럼

기준점을 찾아 놓고, 배열에서 제거한다.

바깥쪽 점들을 찾을 건데, 점을 반시계 방향순으로 탐색 할 것이다.

그러니, 그 점(central)을 기준으로, 모든 점을 반시계 방향으로 정렬을 시도 하자!


        al.sort(new Comparator<Point>() {

            @Override

            public int compare(Point o1, Point o2) {

                return CCW(central, o1, o2);

            }

        });

어렵다고 느낄 수 있지만, 극복해야 한다.

ccw를 이용해 그 점의 상대적인 각도를 알 수 있다. o1, o2가 반시계 관계라면, central 에서 반시계로 돌릴 때, o1을 먼저 검사하게 되는 것이다.

6시 방향부터, 반시계로 돌면서 central을 기준으로 탐색을 시작할 수 있게 되었다.


        Stack<Point> s = new Stack<>();

        s.push(central);



        for (int i=0;i<al.size();i++) {

            s.push(al.get(i));

            while(s.size()>2) {

                Point c = s.pop(), b = s.pop(), a = s.peek();

                int ccw = CCW(a, b, c);

                // ccw 가 반시계 일 때 -1을 리턴한다.

                if (ccw < 0) {

                    s.push(b);

                    s.push(c);

                    break;

                }

                //ccw가 1이나 0 을리턴하여, a -> b -> c 순서로 시계방향임이 입증되었다. 가운데 b점을 넣지 않음으로써, 빼게 된다.



                s.push(c);

            }

        }

        Point b = s.pop();

        if (CCW(s.peek(), b, central) < 0)

            s.push(b);

이제 스택에 넣어주면서 반시계로 탐색을 하는데, 시계방향으로 향하게 된다면, 반시계가 될 때까지, 스택에서 계속 제거해주면서 해주면 된다.

이해하는 것 자체가 플레티넘인 것이다. 이런 기하학적 발상을 코드로 이해한다는 건 어려운게 맞다.

image.png

그림을 보면 2번 3번 4번을 순서대로 연결 하면, 볼록한게 아니라, 들어가 버리게 된다...

들어가는 것 자체가 2, 3, 4를 CCW 하게되면, 시계방향이 나오는 것이다.

그러니 3번을 제거하고 4번을 담은다음 계속 검사를 하는 것이다.

근데 왜 stack이 2 이상일 때, 조건이 맞을 때까지 검사를 하는 것일까?

fawefawef.png

이러한 점의 집합이 있다고 해보자. 여기서 convex hull (볼록 껍질 ) 볼록 다각형을 이루는 꼭짓점을 찾을 것이다.

fawefawef.png

fawefawef.png

어찌 잘 진행하여, 이 상황 까지 왔다.

왼쪽 위 점을 연결 시도 해보자.

fawefawef.png

시계방향이다. 가운데 점을 빼고 다시 검사 한다.

fawefawef.png

또 시계 방향이다.

fawefawef.png

또 또 시계 방향이다.

fawefawef.png

드디어 반시계를 찾았다. 그럼 이제 검사 하느라 빼놧던것을 다시 다 넣고, 다시 진행하자.

어? 그런데 검사가 끝났다. 마지막 시작점과의 관계도 체크해준다.

fawefawef.png

이 개념을 이해하여, 코드를 보며 이해하도록 해보자.

마지막에 b를 다시 뺏다가 넣는것은, 이 b라는 점이 시작점 central과 연결 되었을 때, 볼록한지 체크해야 하기 때문이다.

이렇게 스택에 볼록다각형을 이루는 꼭짓점들을 다 담았다.

int 형으로 안된다면, long 형으로 변환하여, 오버플로우를 예방하도록 하자. 오버플로우가 나는 순간 관계가 변하게 된다. 주의하자

사실 이 알고리즘은 O(N log N) 이다.

정렬에서 시간을 잡아 먹기 때문이다. ㅎ

이 알고리즘을 활용해, 점의 집합에서, 가장 먼 두 점을 구할 수도 있고, 아무튼 요긴하게 쓰인다.

자바로 할 때, Arraylist를 쓰는 것을 권장한다.

자 이제 플레티넘 문제를 풀러가자

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

댓글

이 블로그의 인기 게시물

SW - IT 충남대 콘테스트 문제 풀이 + 후기

백준 16748 Colorful Tree