컴퓨터 구조 운영체제 네트워크 자료구조 알고리즘 데이터베이스 내 학습
algo · 07

그리디

알고리즘그리디최적화

그리디(Greedy) 알고리즘은 매 순간 지금 당장 가장 좋아 보이는 선택을 하고, 그 선택을 절대 번복하지 않는 전략입니다. algo-06(DP 기초)의 배낭 문제처럼 "모든 경우를 비교"하지 않고 바로 결정하기 때문에 훨씬 빠르지만, 항상 최적해를 보장하지는 않습니다.

핵심 개념

거스름돈 문제

거슬러줄 금액이 남아있는 한, 그 금액을 넘지 않는 가장 큰 동전을 계속 사용합니다. 한 번 사용한 동전은 되돌리지 않습니다.

핵심 개념

활동 선택 문제

겹치지 않는 활동을 최대한 많이 고르는 문제입니다. "종료 시간이 가장 빠른 활동"부터 순서대로 검토하며, 이미 선택한 활동과 겹치지 않으면 무조건 선택합니다.

핵심 개념

왜 종료 시간 기준인가

활동을 일찍 끝낼수록 남은 시간에 다른 활동을 더 많이 넣을 자리가 생깁니다. 그래서 "언제 시작하는가"가 아니라 "언제 끝나는가"가 유리한 정렬 기준입니다.

핵심 개념

되돌리지 않는다(번복 없음)

DP는 "포함 vs 제외" 두 경우를 모두 계산해 비교하지만, 그리디는 한 번 결정하면 끝입니다. 이 "번복 없음"이 그리디를 빠르게 만들지만, 동시에 최적해를 놓칠 위험이기도 합니다.

핵심 개념

그리디가 항상 통하지는 않는다

algo-06의 0/1 배낭 문제에서 "가치가 가장 높은 아이템부터 담기"는 그리디지만 최적해를 보장하지 못합니다. 반면 이 토픽의 두 문제는 이 시각화의 조건에서 그리디로 최적해가 나옵니다.

핵심 개념

시간복잡도

거스름돈은 동전 종류만큼, 활동 선택은 정렬 O(n log n) 이후 한 번 훑기만 하면 됩니다(O(n)). 모든 조합을 비교하는 DP보다 훨씬 빠릅니다.

그리디가 통하는 조건 — "지금 당장 최선의 선택"들을 모으면 "전체적으로도 최선"이 되는 성질(탐욕적 선택 속성)이 있어야 합니다. 활동 선택 문제는 이 성질을 만족한다고 증명되어 있고, 거스름돈은 이 시각화의 동전 단위에서 성립합니다.
거스름돈의 함정 — 이 시각화의 동전(500, 100, 50, 10원)은 큰 단위가 작은 단위의 배수라 그리디가 최적이 되는 경우입니다(배수가 아니어도 그리디가 최적인 동전 체계가 있고, 배수가 아니면 깨질 수도 있습니다). 하지만 동전이 500원, 400원, 100원처럼 애매한 단위라면, 그리디가 최소 개수를 못 찾을 수 있습니다 — 이런 경우엔 DP(algo-06 방식)가 필요합니다.
왜 시작 시간 기준으로 정렬하면 안 되는가 — "가장 일찍 시작하는 활동"부터 고르면, 그 활동이 아주 길게 끝나서 뒤에 올 수 있었던 여러 짧은 활동들을 다 막아버릴 수 있습니다. "일찍 끝내는 것"이 자리를 더 많이 남겨줍니다.
이 시각화에서 확인할 것 — "거스름돈" 탭에서 큰 동전부터 순서대로 소진되는 과정을, "활동 선택" 탭에서 초록 기준선(마지막 선택 활동의 종료 시간)을 넘는 활동만 선택되는 과정을 확인하세요.

그리디가 통하는 때와 아닐 때

그리디 알고리즘은 매 단계에서 지금 가장 좋아 보이는 선택을 하고 그것을 바꾸지 않는 방법입니다. 빠르고 단순하지만 어떤 문제에서든 최적이 되는 것은 아니어서, 쓰기 전에 그 문제에서 그리디가 맞다는 근거를 따져 봐야 합니다.

숫자로 확인하기

이 시각화의 거스름돈 — 860원을 500, 100, 50, 10원 동전으로 거슬러 주면 500원 1개, 100원 3개, 50원 1개, 10원 1개로 모두 6개를 씁니다.
그리디가 실패하는 예 — 동전이 500, 400, 100원일 때 800원을 그리디로 주면 500 + 100 + 100 + 100으로 4개이지만, 400원 2개면 2개로 끝납니다.
활동 선택 — 활동 6개(A 1~3, B 2~5, C 4~7, D 1~8, E 5~9, F 8~10)에서 종료 시간이 빠른 순으로 고르면 A, C, F로 3개입니다. 가장 길게 걸리는 D를 먼저 고르면 D와 F로 2개뿐입니다.

자주 하는 오해

그리디는 항상 최적해를 준다 — 문제가 그리디 선택 속성과 최적 부분 구조를 가질 때만 최적이 보장됩니다. 그렇지 않으면 근사치에 그치거나 틀립니다.
거스름돈은 항상 큰 동전부터 주면 된다 — 동전 단위에 따라 다릅니다. 이 시각화의 단위(500·100·50·10원)는 맞지만 500·400·100원은 위처럼 실패합니다.
정렬 기준은 직관대로 고르면 된다 — 활동 선택에서는 시작 시간이나 길이가 아니라 종료 시간이 정답 기준입니다. 기준에 따라 결과가 달라지므로 증명이 필요합니다.
그리디는 DP의 단순화 버전이다 — 둘은 다른 방식입니다. 그리디는 한 번 고르고 끝내고, DP는 가능한 선택을 모두 비교합니다. 그리디가 통하는 문제는 DP보다 빠르게 풀 수 있습니다.

면접에서 이렇게 물어요

면접 질문

그리디가 맞는지 어떻게 확인하나요?

그리디로 고른 선택을 최적해에도 포함시킬 수 있다는 것(그리디 선택 속성)과, 선택 후 남은 문제도 같은 구조라는 것(최적 부분 구조)을 보입니다. 보통 교환 논법으로 증명합니다.

면접 질문

거스름돈에서 그리디가 안 되는 반례를 들어 보세요.

동전이 500, 400, 100원이고 800원을 줄 때 그리디는 4개(500 + 100 × 3), 최적은 400원 2개입니다.

면접 질문

활동 선택에서 왜 종료 시간이 빠른 순인가요?

일찍 끝나는 활동을 고르면 이후에 쓸 수 있는 시간이 가장 많이 남습니다. 시작이 빠른 활동은 길게 이어져 뒤의 여러 활동을 막을 수 있습니다.

면접 질문

분할 가능 배낭은 그리디로 풀리는데 0/1 배낭은 왜 안 되나요?

분할 가능하면 무게 대비 가치가 높은 순으로 담다가 마지막에 쪼갤 수 있어서 최적입니다. 0/1은 쪼갤 수 없어 남는 용량이 낭비될 수 있고, 그래서 DP가 필요합니다.

실제 시스템에서는

허프만 부호 — DEFLATE(RFC 1951)는 LZ77과 허프만 부호를 함께 씁니다. 허프만 부호를 만드는 알고리즘은 가장 빈도가 낮은 두 노드를 합치는 그리디 방식으로 알려져 있습니다.
그래프 알고리즘 — 다익스트라, 크루스칼, 프림은 모두 그리디 선택을 쓰는 알고리즘입니다. 최소 신장 트리 토픽에서 같은 그리디 선택이 항상 최적이 되는 사례를 봅니다.

함께 보면 좋은 토픽

DP 기초 — 그리디가 못 푸는 0/1 배낭을 DP로 풀어 봅니다.
최소 신장 트리 — 그리디 선택이 항상 최적인 대표 문제입니다.
다익스트라 — 그리디 선택으로 최단 거리를 구하는 알고리즘입니다.
백트래킹 — 선택을 되돌리는 방식과 대비해 봅니다.

참고 자료

교재 — Cormen, Leiserson, Rivest, Stein, "Introduction to Algorithms" 4판, "Greedy Algorithms" 장(활동 선택, 허프만 부호, 분할 가능 배낭)

Java — 거스름돈 (그리디)

/* ── 큰 동전부터 확인하며, 쓸 수 있으면 계속 사용 ──────────── */
public List<Integer> makeChange(int amount, int[] coins) {
    // coins는 반드시 큰 단위 → 작은 단위 순으로 정렬되어 있어야 함
    List<Integer> used = new ArrayList<>();
    for (int coin : coins) {
        while (amount >= coin) {
            used.add(coin);
            amount -= coin;              // 결정을 절대 되돌리지 않음
        }
    }
    return used;                     // amount가 0이 되면 끝
}

반복문이 두 겹이지만, 안쪽 while은 동전 하나당 최대 몇 번만 반복되므로 전체 시간은 동전 종류 수에 비례합니다 — DP처럼 표를 채우지 않아 훨씬 빠릅니다.
이 코드가 항상 최소 개수를 보장하는 것은 coins 배열의 단위 관계(예: 500=100×5) 덕분입니다. 단위가 애매하면 이 가정이 깨집니다.

Java — 활동 선택 (그리디)

/* ── 종료 시간 기준 정렬 후, 겹치지 않으면 무조건 선택 ────── */
public List<Activity> selectActivities(List<Activity> activities) {
    activities.sort((a, b) -> a.end - b.end);   // 핵심: 종료 시간 오름차순 정렬

    List<Activity> selected = new ArrayList<>();
    int lastEnd = -1;
    for (Activity a : activities) {
        if (a.start >= lastEnd) {     // 마지막 선택과 안 겹치면
            selected.add(a);          // 무조건 선택 (되돌리지 않음)
            lastEnd = a.end;
        }
    }
    return selected;
}

a.start >= lastEnd 한 줄이 "겹치는지" 판단의 전부입니다 — 겹치지 않으면 무조건 선택하고, 그 선택을 절대 재검토하지 않습니다.
정렬에 O(n log n), 이후 한 번 훑는 데 O(n)이 걸려 전체 O(n log n)입니다. 모든 부분집합을 확인해야 하는 방식보다 압도적으로 빠릅니다.

Java — 그리디가 실패하는 예 (0/1 배낭 문제)

/* ── "가치 높은 순"으로 그리디하게 담으면? 최적해가 아닐 수 있음 ── */
public int greedyKnapsack(int[] weights, int[] values, int capacity) {
    // 가치 기준 내림차순 정렬 후, 담을 수 있으면 무조건 담기 (그리디)
    Integer[] idx = /* 가치 내림차순으로 정렬된 인덱스라고 가정 */ null;
    int totalValue = 0;
    for (int i : idx) {
        if (weights[i] <= capacity) {
            capacity -= weights[i];
            totalValue += values[i];     // 한 번 담으면 되돌리지 않음
        }
    }
    return totalValue;                  // algo-06의 DP 결과보다 작을 수 있음!
}

이 코드는 문법적으로는 멀쩡하지만, "가치가 높다고 먼저 담은 아이템"이 무게를 많이 차지해 뒤에 더 효율적인 조합을 막아버릴 수 있습니다.
algo-06(DP 기초)에서 같은 문제를 dp[i][w] 표로 풀었을 때와 결과를 비교해보면, 그리디가 항상 통하지 않는다는 것을 직접 확인할 수 있습니다 — "포함/제외를 전부 비교하지 않고 하나로 확정해버린" 대가입니다.

Q1

그리디(탐욕) 알고리즘의 기본 전략은?

'당장 최선'을 반복하는 전략입니다.
그리디 알고리즘은 각 단계에서 지금 당장 가장 좋아 보이는 선택을 하고, 이전 선택을 번복하지 않고 진행합니다.

Q2

동전 거스름돈 문제에서 그리디가 항상 최적해를 보장하지 못하는 경우가 생기는 이유는?

특정 동전 단위 조합에서는 큰 동전 우선이 오히려 손해일 수 있습니다.
500/100/50/10원처럼 배수 관계가 잘 맞는 단위에서는 그리디가 최적이지만, 동전 단위 구성에 따라(예: 1,3,4원) 큰 동전 우선 선택이 전체 최소 개수를 보장하지 못하는 경우가 있습니다.

Q3

활동 선택 문제(회의실 배정)에서 그리디가 최적인 이유로 자주 드는 기준은?

빨리 끝나는 일을 먼저 잡으면 이후 선택지가 더 많이 남습니다.
종료 시간이 가장 빠른 활동부터 선택하면 다음 활동을 고를 수 있는 남은 시간이 최대화되어, 전체적으로 더 많은 활동을 선택할 수 있음이 증명되어 있습니다.