지금 당장 가장 좋아 보이는 것을 선택하면, 전체적으로도 최적이 될까요? 그리디 알고리즘은 이 질문에 "예"라고 답할 수 있는 경우에 사용합니다.

개념 정의

그리디(Greedy) 알고리즘은 각 단계에서 현재 상황에서 가장 좋아 보이는 선택 을 하는 방법입니다. 한 번 선택하면 되돌아가지 않습니다.

그리디가 성립하기 위한 조건

1. 탐욕 선택 속성 (Greedy Choice Property)

현재 시점에서의 최적 선택이 전체 최적해에 포함됩니다.

2. 최적 부분 구조 (Optimal Substructure)

전체 문제의 최적해가 부분 문제의 최적해를 포함합니다. (DP와 공유하는 성질)

이 두 조건이 모두 만족할 때만 그리디가 최적해를 보장합니다.

대표 예제 1: 활동 선택 문제 (Activity Selection)

여러 활동이 시작 시간과 종료 시간을 가질 때, 겹치지 않으면서 최대한 많은 활동 을 선택하는 문제입니다.

JAVA
public static int activitySelection(int[][] activities) {
    // 종료 시간 기준 정렬
    Arrays.sort(activities, Comparator.comparingInt(a -> a[1]));

    int count = 1;
    int lastEnd = activities[0][1];

    for (int i = 1; i < activities.length; i++) {
        if (activities[i][0] >= lastEnd) { // 시작 시간 >= 이전 종료 시간
            count++;
            lastEnd = activities[i][1];
        }
    }
    return count;
}

왜 종료 시간으로 정렬하는가?

종료 시간이 빠른 활동을 먼저 선택하면 남은 시간이 최대화됩니다. 이것이 탐욕 선택 속성입니다.

대표 예제 2: 거스름돈 문제

JAVA
public static int coinChange(int[] coins, int amount) {
    Arrays.sort(coins);
    int count = 0;

    for (int i = coins.length - 1; i >= 0; i--) {
        count += amount / coins[i];
        amount %= coins[i];
    }

    return amount == 0 ? count : -1; // 거슬러줄 수 없으면 -1
}

** 주의 **: 동전 단위가 배수 관계일 때만 그리디가 최적입니다.

PLAINTEXT
[500, 100, 50, 10]원 → 그리디 OK (배수 관계)
[1, 3, 4]원, 6원 → 그리디 실패!
  그리디: 4+1+1 = 3개
  최적:   3+3   = 2개 (DP 필요)

대표 예제 3: 분할 가능한 배낭 (Fractional Knapsack)

물건을 쪼갤 수 있는 배낭 문제는 그리디로 해결됩니다.

JAVA
public static double fractionalKnapsack(int capacity, int[][] items) {
    // 무게 대비 가치(단가) 기준 정렬
    Arrays.sort(items, (a, b) ->
        Double.compare((double) b[1] / b[0], (double) a[1] / a[0])
    );

    double totalValue = 0;
    int remaining = capacity;

    for (int[] item : items) {
        int weight = item[0], value = item[1];

        if (remaining >= weight) {
            totalValue += value;
            remaining -= weight;
        } else {
            totalValue += (double) value / weight * remaining;
            break;
        }
    }
    return totalValue;
}

**0/1 배낭 **(물건을 쪼갤 수 없는)은 그리디가 안 되고 DP가 필요합니다.

대표 예제 4: 허프만 코딩 (Huffman Coding)

빈도가 높은 문자에 짧은 코드를 할당하여 전체 인코딩 길이를 최소화합니다.

JAVA
public static Map<Character, String> huffmanEncoding(Map<Character, Integer> freq) {
    // min-heap: 빈도가 낮은 노드부터
    PriorityQueue<int[]> pq = new PriorityQueue<>(
        Comparator.comparingInt(a -> a[0])
    );

    // 각 문자를 리프 노드로
    for (var entry : freq.entrySet()) {
        pq.offer(new int[]{entry.getValue(), entry.getKey()});
    }

    // 가장 빈도가 낮은 두 노드를 합치기를 반복
    while (pq.size() > 1) {
        int[] left = pq.poll();
        int[] right = pq.poll();
        pq.offer(new int[]{left[0] + right[0], -1}); // 내부 노드
    }

    // 트리를 순회하며 코드 할당 (실제 구현은 트리 구조 필요)
    return buildCodes(pq.poll());
}

허프만 코딩은 gzip, DEFLATE, JPEG 등 다양한 압축 알고리즘의 기반입니다.

그리디 vs DP 판별법

기준그리디DP
선택현재 최적 선택 후 진행모든 선택지를 고려
되돌림없음있음 (부분 문제 결합)
증명탐욕 선택 속성 증명 필요점화식 수립
속도일반적으로 빠름상태 수에 비례

판별 팁

  1. ** 현재 선택이 미래 선택에 영향을 주지 않는가?** → 그리디 가능
  2. ** 쪼갤 수 있는가?** → 그리디 (분할 배낭) / ** 쪼갤 수 없는가?** → DP (0/1 배낭)
  3. ** 최적 해의 구조가 단순한가?** → 그리디 / ** 여러 경우를 비교해야 하는가?** → DP

그리디를 시도하고 반례를 찾는 방법

PLAINTEXT
1. 직감적으로 그리디 전략을 세운다
2. 작은 예제로 테스트한다
3. 반례가 있으면 DP로 전환한다
4. 반례가 없으면 증명을 시도한다 (교환 논증)

교환 논증 (Exchange Argument)

그리디의 정당성을 증명하는 대표적인 방법입니다.

  1. 최적해 OPT가 있다고 가정
  2. 그리디 해 G와 OPT가 다른 부분을 찾는다
  3. OPT에서 그 부분을 그리디 선택으로 교환해도 최적성이 유지됨을 보인다
  4. 따라서 그리디 해도 최적이다

실용적인 그리디 문제들

구간 스케줄링 (회의실 배정)

JAVA
// 하나의 회의실에서 최대한 많은 회의 배정
// = 활동 선택 문제 → 종료 시간 정렬 + 그리디

구간 커버링 (최소 회의실 수)

JAVA
// 모든 회의를 진행하기 위한 최소 회의실 수
public static int minMeetingRooms(int[][] intervals) {
    Arrays.sort(intervals, Comparator.comparingInt(a -> a[0]));

    PriorityQueue<Integer> endTimes = new PriorityQueue<>();

    for (int[] interval : intervals) {
        if (!endTimes.isEmpty() && endTimes.peek() <= interval[0]) {
            endTimes.poll(); // 기존 회의실 재사용
        }
        endTimes.offer(interval[1]); // 회의실 배정
    }

    return endTimes.size();
}

문자열 압축

JAVA
// Run-Length Encoding도 그리디의 일종
// "aaabbc" → "a3b2c1"

백엔드/실무에서의 연결

로드 밸런서의 라운드 로빈

가장 단순한 로드 밸런싱 전략(라운드 로빈)은 "다음 서버에 요청을 보내는" 그리디 선택입니다.

캐시 교체 정책

LRU(Least Recently Used)는 "가장 오래 사용하지 않은 것을 제거하는" 그리디 전략입니다. 최적은 아니지만 실용적입니다.

스케줄러

OS의 프로세스 스케줄러(SJF: Shortest Job First)는 그리디 알고리즘입니다. 가장 짧은 작업을 먼저 실행하여 평균 대기 시간을 최소화합니다.

데이터 압축

gzip, Brotli 등의 압축 알고리즘 내부에서 허프만 코딩(그리디)이 사용됩니다.

주의할 점

그리디가 최적해를 보장하는지 증명 없이 사용하는 실수

그리디가 직관적으로 맞아 보여도, 탐욕 선택 속성이 만족되지 않으면 최적해를 놓칩니다. 동전 [1, 3, 4]에서 6원의 경우, 그리디는 4+1+1 = 3개이지만 최적은 3+3 = 2개입니다.

정렬 기준을 잘못 설정하는 실수

활동 선택 문제에서 시작 시간이 아닌 종료 시간으로 정렬해야 합니다. 정렬 기준이 잘못되면 그리디의 정당성이 깨집니다.


정리

문제그리디 전략정렬 기준
활동 선택종료 빠른 것 먼저종료 시간 오름차순
거스름돈큰 동전 먼저동전 크기 내림차순
분할 배낭단가 높은 것 먼저가치/무게 내림차순
허프만 코딩빈도 낮은 것 먼저 합치기빈도 오름차순

기억할 포인트:

  • 그리디는 ** 현재 최적 선택이 전체 최적에 포함될 때** 사용합니다.
  • 탐욕 선택 속성이 성립하지 않으면 반례가 존재합니다. DP를 고려하세요.
  • "정렬 + 순서대로 선택" 패턴이 그리디의 가장 흔한 형태입니다.
  • 동전 문제처럼 직관적으로 그리디 같지만 아닌 경우를 주의하세요.
댓글 로딩 중...