🧭 BOJ 1708 - 볼둝 껍질 (Graham Scan)

κΉ€μž¬μ—°Β·2026λ…„ 3μ›” 1일

πŸ“Œ 문제 κ°œμš”

2차원 평면 μœ„μ— N개의 점이 μ£Όμ–΄μ§„λ‹€.
이 점듀 쀑 일뢀λ₯Ό 선택해 볼둝 λ‹€κ°ν˜•(Convex Polygon) 을 λ§Œλ“€κ³ ,
κ·Έ λ‹€κ°ν˜•μ„ μ΄λ£¨λŠ” 점의 개수λ₯Ό κ΅¬ν•˜λŠ” λ¬Έμ œμ΄λ‹€.

문제의 핡심 쑰건은 λ‹€μŒκ³Ό κ°™λ‹€.

  • λ‹€κ°ν˜•μ˜ λͺ¨λ“  내각은 180도 μ΄ν•˜
  • μ„ νƒν•œ μ λ“€λ‘œ λ§Œλ“  λ‹€κ°ν˜•μ€ λͺ¨λ“  점을 포함해야 ν•œλ‹€

즉, μš°λ¦¬λŠ” Convex Hull (볼둝 껍질) 을 ꡬ해야 ν•œλ‹€.


✨ μ ‘κ·Ό 방법 β€” Graham Scan

이 λ¬Έμ œλŠ” λŒ€ν‘œμ μΈ 볼둝 껍질 μ•Œκ³ λ¦¬μ¦˜μΈ Graham Scan 으둜 ν•΄κ²°ν•  수 μžˆλ‹€.

Graham Scan의 전체 흐름은 λ‹€μŒκ³Ό κ°™λ‹€.

  1. 기쀀점(root)을 μ •ν•œλ‹€
  2. 기쀀점을 κΈ°μ€€μœΌλ‘œ λ°˜μ‹œκ³„ λ°©ν–₯ 정렬을 ν•œλ‹€
  3. μŠ€νƒμ„ μ΄μš©ν•΄ 항상 β€œμ’ŒνšŒμ „(λ°˜μ‹œκ³„)β€λ§Œ μœ μ§€ν•œλ‹€
  4. μ‹œκ³„ λ°©ν–₯이 λ‚˜μ˜€λ©΄ 직전 점을 μ œκ±°ν•œλ‹€

이 과정을 톡해 μžμ—°μŠ€λŸ½κ²Œ κ°€μž₯ λ°”κΉ₯ μ λ“€λ§Œ λ‚¨κ²Œ λœλ‹€.


1️⃣ 기쀀점(root) 선택

기쀀점은 λ‹€μŒ κΈ°μ€€μœΌλ‘œ μ„ νƒν•œλ‹€.

  • y 값이 κ°€μž₯ μž‘μ€ 점
  • y 값이 κ°™λ‹€λ©΄ x 값이 κ°€μž₯ μž‘μ€ 점

이 점은 λ°˜λ“œμ‹œ 볼둝 κ»μ§ˆμ— ν¬ν•¨λœλ‹€.
κ°€μž₯ μ•„λž˜μ— μœ„μΉ˜ν•œ μ μ΄λ―€λ‘œ 내뢀에 포함될 수 μ—†κΈ° λ•Œλ¬Έμ΄λ‹€.

μ½”λ“œλŠ” λ‹€μŒκ³Ό 같이 κ΅¬ν˜„ν•˜μ˜€λ‹€.

for (int i = 0; i < points.size(); i++) {
    if (points.get(i).y < root.y) {
        root = points.get(i);
    } else if (points.get(i).y == root.y) {
        if (points.get(i).x < root.x) {
            root = points.get(i);
        }   
    }
}

2️⃣ λ°˜μ‹œκ³„ λ°©ν–₯ μ •λ ¬

이제 기쀀점을 μ€‘μ‹¬μœΌλ‘œ λ‚˜λ¨Έμ§€ 점듀을 λ°˜μ‹œκ³„ λ°©ν–₯(CCW) 으둜 μ •λ ¬ν•œλ‹€.

μ—¬κΈ°μ„œ μ‚¬μš©ν•˜λŠ” 것이 ccw() 연산이닀.

ccw(p1, p2, p3)의 μ˜λ―ΈλŠ” λ‹€μŒκ³Ό κ°™λ‹€.

  • μ–‘μˆ˜ β†’ λ°˜μ‹œκ³„ λ°©ν–₯
  • 음수 β†’ μ‹œκ³„ λ°©ν–₯
  • 0 β†’ 일직선(평행)

μ •λ ¬ 기쀀은 λ‹€μŒκ³Ό κ°™λ‹€.

  • λ°˜μ‹œκ³„ λ°©ν–₯에 μžˆλŠ” 점이 μ•žμœΌλ‘œ μ˜€λ„λ‘ ν•œλ‹€
  • λ§Œμ•½ ccw 값이 0이라면 (평행)
    β†’ κΈ°μ€€μ μ—μ„œ 더 κ°€κΉŒμš΄ 점이 λ¨Όμ € μ˜€λ„λ‘ ν•œλ‹€

μ½”λ“œλŠ” λ‹€μŒκ³Ό κ°™λ‹€.

Collections.sort(points, (p1, p2) -> {
    int result = ccw(root, p1, p2);
    
    if (result > 0) {
        return -1;
    } else if (result < 0) {
        return 1;
    } else {
        long distance1 = dist(root, p1);
        long distance2 = dist(root, p2);
    
        if (distance1 > distance2) {
            return 1;
        } else {
            return -1;
        }
    }
});

μ—¬κΈ°μ„œ ν‰ν–‰ν•œ 경우 κ°€κΉŒμš΄ 점을 λ¨Όμ € λ‘λŠ” μ΄μœ λŠ”
이후 μŠ€νƒ 처리 κ³Όμ •μ—μ„œ μžλ™μœΌλ‘œ λ¨Ό 점만 남기기 μœ„ν•¨μ΄λ‹€.


3️⃣ μŠ€νƒμ„ μ΄μš©ν•œ 볼둝성 μœ μ§€

이제 μ •λ ¬λœ 점듀을 μˆœμ„œλŒ€λ‘œ λ³΄λ©΄μ„œ 볼둝 λ‹€κ°ν˜•μ„ κ΅¬μ„±ν•œλ‹€.

핡심 μ•„μ΄λ””μ–΄λŠ” λ‹€μŒκ³Ό κ°™λ‹€.

λ§ˆμ§€λ§‰ 두 점과 μƒˆ 점이
λ°˜μ‹œκ³„ λ°©ν–₯이 아닐 경우 μ œκ±°ν•œλ‹€.

κ΅¬ν˜„ μ½”λ“œλŠ” λ‹€μŒκ³Ό κ°™λ‹€.

Stack<Point> stack = new Stack<>();
stack.add(root);

for (int i = 1; i < points.size(); i++) {
    while (stack.size() > 1 && ccw(stack.get(stack.size() - 2), stack.get(stack.size() - 1), points.get(i)) <= 0) {
        stack.pop();
    }
    stack.add(points.get(i));
}

πŸ” 이 μ½”λ“œμ˜ 의미

  • stackμ—λŠ” ν˜„μž¬κΉŒμ§€ μ„ νƒλœ 볼둝 껍질의 꼭짓점듀이 λ“€μ–΄κ°„λ‹€.
  • λ§ˆμ§€λ§‰ 두 점과 μƒˆ 점이
    • μ‹œκ³„ λ°©ν–₯μ΄κ±°λ‚˜
    • 일직선이라면
  • μ΄λŠ” λ‚΄λΆ€λ‘œ κΊΎμ΄λŠ” ν˜•νƒœμ΄λ―€λ‘œ μ œκ±°ν•œλ‹€.

이 과정을 λ°˜λ³΅ν•˜λ©΄ 항상 β€œμ’ŒνšŒμ „β€λ§Œ μœ μ§€λ˜κ²Œ λœλ‹€.
즉, λͺ¨λ“  내각이 180도 μ΄ν•˜κ°€ λœλ‹€.


4️⃣ ν‰ν–‰ν•œ 점 처리

μ •λ ¬ λ‹¨κ³„μ—μ„œ 같은 λ°©ν–₯(평행)일 경우 κ°€κΉŒμš΄ 점을 λ¨Όμ € λ°°μΉ˜ν–ˆλ‹€.

이후 μŠ€νƒ λ‹¨κ³„μ—μ„œ

  • 더 λ¨Ό 점이 λ“€μ–΄μ˜¬ λ•Œ
  • ccw == 0이 λ˜μ–΄
  • κ°€κΉŒμš΄ 점이 pop λœλ‹€

결과적으둜 같은 직선 μœ„μ—μ„œλŠ” κ°€μž₯ λ¨Ό 점만 λ‚¨κ²Œ λœλ‹€.


5️⃣ μ™œ λͺ¨λ“  점을 ν¬ν•¨ν•˜λŠ”κ°€?

기쀀점을 κ°€μž₯ μ•„λž˜ 점으둜 μ„ νƒν–ˆκ³ ,
항상 λ°˜μ‹œκ³„ λ°©ν–₯만 μœ μ§€ν•˜λ„λ‘ κ΅¬μ„±ν–ˆκΈ° λ•Œλ¬Έμ—

  • λ‚΄λΆ€ 점은 λ°˜λ“œμ‹œ μ–΄λŠ μˆœκ°„ μ‹œκ³„ λ°©ν–₯을 λ§Œλ“€κ²Œ 되고
  • κ·Έ μˆœκ°„ pop λ˜μ–΄ μ œκ±°λœλ‹€

κ²°κ΅­ λ‚¨λŠ” 점듀은 κ°€μž₯ λ°”κΉ₯을 κ°μ‹ΈλŠ” 점듀뿐이닀.

즉, μ΅œμ’… stack에 남아 μžˆλŠ” 점듀이 λ°”λ‘œ Convex Hull이닀.


πŸ”’ 전체 μ½”λ“œ

(μ½”λ“œ 뢀뢄은 κ·ΈλŒ€λ‘œ μœ μ§€)

// 전체 μ½”λ“œ μƒλž΅ 없이 κ·ΈλŒ€λ‘œ μ‚½μž…
import java.util.*;
import java.io.*;
import java.util.function.Function;

public class Main {
    static List<Point> points = new ArrayList<>();
    static Point root = new Point(Long.MAX_VALUE, Long.MAX_VALUE);

    static class Point {
        long y;
        long x;

        Point(long y, long x) {
            this.y = y;
            this.x = x;
        }
    }

    static int solve() {
        for (int i = 0; i < points.size(); i++) {
            if (points.get(i).y < root.y) {
                root = points.get(i);
            } else if (points.get(i).y == root.y) {
                if (points.get(i).x < root.x) {
                    root = points.get(i);
                }
            }
        } // 루트 값은 정함

        Collections.sort(points, (p1, p2) -> {
            int result = ccw(root, p1, p2);

            if (result > 0) {
                return -1;
            } else if (result < 0) {
                return 1;
            } else {
                long dist1 = distance(root, p1);
                long dist2 = distance(root, p2);

                if (dist1 > dist2) {
                    return 1;
                }

                return -1;
            }
        });

        Stack<Point> stack = new Stack<>();
        stack.add(root);

        for (int i = 1; i < points.size(); i++) {
            while (stack.size() > 1 && ccw(stack.get(stack.size() - 2), stack.get(stack.size() - 1), points.get(i)) <= 0) {
                stack.pop();
            }

            stack.add(points.get(i));
        }

        return stack.size();
    }

    static int ccw(Point p1, Point p2, Point p3) {
        long angle = ((p1.x * p2.y) + (p2.x * p3.y) + (p3.x * p1.y))
                - ((p1.y * p2.x) + (p2.y * p3.x) + (p3.y * p1.x));

        return (angle == 0) ? 0
                : (angle > 0) ? 1 : -1;
    }

    static long distance(Point p1, Point p2) {
        return ((p2.y - p1.y) * (p2.y - p1.y)) + ((p2.x - p1.x) * (p2.x - p1.x));
    }

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        Function<String, Integer> fun = Integer::parseInt;

        int N = fun.apply(br.readLine());

        for (int i = 0; i < N; i++) {
            String[] input = br.readLine().split(" ");
            int a = fun.apply(input[0]);
            int b = fun.apply(input[1]);

            points.add(new Point(a, b));
        }

        System.out.println(solve());
    }
}

πŸ’‘ 마무리

μ²˜μŒμ—λŠ” μ§κ΄€μ μœΌλ‘œ μ΄ν•΄ν•˜κΈ° μ–΄λ €μ› μ§€λ§Œ,
핡심은 단 ν•˜λ‚˜μ˜€λ‹€.

"항상 μ’ŒνšŒμ „λ§Œ ν—ˆμš©ν•œλ‹€."

이 μ›λ¦¬λ§Œ μ΄ν•΄ν•˜λ©΄
Graham Scan은 ꡉμž₯히 μš°μ•„ν•œ μ•Œκ³ λ¦¬μ¦˜μ΄λΌλŠ” 것을 λŠλ‚„ 수 μžˆλ‹€.

κΈ°ν•˜ 문제 μ€‘μ—μ„œλ„ ꡬ쑰λ₯Ό μ΄ν•΄ν•˜λŠ” μž¬λ―Έκ°€ μžˆλŠ” λ¬Έμ œμ˜€λ‹€.

profile
λŠμž„μ—†μ΄ 'μ„±μž₯'ν•˜λŠ” 개발자 κΉ€μž¬μ—°μž…λ‹ˆλ‹€.

0개의 λŒ“κΈ€