0Pricing
Coding Interview Prep · 강의

그리디와 DP: 각각 언제 사용할까

그리디 선택 속성과 교환 논증을 사용해 그리디로 해결할 수 있는 문제와 DP가 필요한 문제의 특징을 구분합니다.

그리디와 DP: 각각 언제 사용할까은(는) CoddyKit의 무료 Coding Interview Prep 강의입니다. 이것은 4개 중 1번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Coding Interview Prep 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Coding Interview Prep 강의에는 총 4개의 강의가 포함되어 있습니다.

탐욕법과 DP 개요

탐욕법과 동적 계획법은 모두 최적화 문제, 즉 최댓값·최솟값 또는 최적의 배치를 찾는 문제를 해결합니다. 탐욕법은 이전 결정을 다시 검토하지 않고 각 단계에서 국소적으로 최적인 선택을 합니다. DP는 모든 가능성을 탐색하지만 메모이제이션을 사용하여 재계산을 피합니다. 어느 방법을 적용할지 알면 잘못된 탐욕법을 디버깅하거나 불필요하게 복잡한 DP 표를 만드는 데 드는 시간을 줄일 수 있습니다.

# Greedy: always take the locally best option
# Example: coin change with coins [1, 5, 10, 25]
# Greedy: take as many 25s as possible, then 10s, etc.
# This works for standard denominations but NOT all coin sets!

# DP: explore all possibilities via memoisation
# Example: coin change with coins [1, 3, 4] and target 6
# Greedy would pick 4, then 1, 1 → 3 coins
# DP finds: 3 + 3 → 2 coins (optimal!)
print('Greedy can fail when local optimum != global optimum')

탐욕 선택 속성

문제에 탐욕 선택 속성이 있다는 것은 국소적으로 최적인 선택을 계속 수행하여 전역적으로 최적인 해를 항상 구성할 수 있다는 뜻입니다. 형식적으로는 탐욕 선택으로 시작하는 최적해가 적어도 하나 존재하므로 backtrack할 필요가 없습니다. 이를 증명할 때는 일반적으로 교환 논증을 사용합니다. 어떤 최적해가 탐욕 선택을 포함하지 않는다고 가정한 뒤, 결과를 악화하지 않고 그 선택으로 바꿀 수 있음을 보이는 방식입니다.

# Exchange argument example: Activity Selection
# Greedy: always pick the activity that ends earliest
# Proof: suppose optimal solution starts with activity A (not earliest-ending)
# Let G be the earliest-ending activity.
# Replace A with G in the solution:
# - G ends no later than A, so G does not conflict with any activity A allowed
# - The solution remains valid with at least as many activities
# Therefore greedy choice (earliest end) is always safe.

activities = [(1,4), (3,5), (0,6), (5,7), (3,9), (5,9), (6,10), (8,11), (8,12), (2,14)]
activities.sort(key=lambda x: x[1])  # sort by end time
print('Sorted by end:', activities[:4], '...')

최적 부분 구조

탐욕법과 DP에는 모두 최적 부분 구조가 필요합니다. 즉, 전체 문제의 최적해가 부분 문제의 최적해를 포함해야 합니다. 두 방법의 차이는 부분 문제의 최적해를 모든 선택지를 탐색하지 않고 탐욕적으로 결정할 수 있는지, 아니면 여러 선택지를 비교해야 하는지에 있습니다. 어떤 선택을 한 뒤 남은 부분 문제가 동일한 구조라면 탐욕법이 작동합니다. 여러 선택지를 비교해야 한다면 DP를 사용하십시오.

# Greedy works: activity selection
# Making the greedy choice (earliest-ending) leaves a sub-problem
# that is structurally identical (activity selection on remaining activities)
# and the greedy choice for the sub-problem is still valid.

# DP needed: 0/1 knapsack
# After choosing to include/exclude item i, the remaining sub-problem
# depends on WHICH item we chose — different choices yield different sub-problems.
# No single greedy rule works for all inputs.

print('Greedy: sub-problem is unique after each choice')
print('DP: sub-problem depends on which choice was made')

겹치는 부분 문제는 DP의 신호

재귀적 분해 과정에서 같은 부분 문제가 여러 번 해결된다면 메모이제이션을 사용하는 DP가 필요합니다. 재귀 트리를 그리고 반복해서 나타나는 노드를 찾아보십시오. 피보나치 수열에서는 fib(5)의 트리에서 fib(3)이 두 번 계산됩니다. 동전 교환에서 동전 [1,3,4]와 목표값 6을 사용하면 목표값 3, 2, 1에 대한 부분 문제가 여러 번 나타납니다. 겹치는 부분 문제와 최적 부분 구조가 함께 있으면 DP를 적용합니다.

# Recursion tree for coin change [1,3,4], target=6
# bt(6) → bt(5) → bt(4) → bt(3) (repeated!)
#              → bt(2) → bt(1) (repeated!)
#         → bt(3) (repeated!)
#       → bt(2) (repeated!)

# Without memoisation: exponential time
# With DP table: O(target * len(coins)) time

def coin_change_dp(coins, amount):
    dp = [float('inf')] * (amount + 1)
    dp[0] = 0
    for a in range(1, amount + 1):
        for c in coins:
            if c <= a:
                dp[a] = min(dp[a], dp[a - c] + 1)
    return dp[amount] if dp[amount] != float('inf') else -1

print(coin_change_dp([1, 3, 4], 6))  # 2 (3+3)
print(coin_change_dp([2], 3))        # -1 (impossible)

대표적인 탐욕법 문제

탐욕법이 올바르다고 증명된 문제는 다음과 같습니다. (1) 활동/구간 스케줄링 — 가장 빠른 종료 시각을 선택하는 탐욕법. (2) 최소 신장 트리 — 프림 알고리즘과 크루스칼 알고리즘. (3) 허프만 부호화 — 항상 빈도가 가장 낮은 두 노드를 병합합니다. (4) 분할 가능 배낭 — 가치/무게 비율이 가장 높은 항목부터 선택합니다. (5) 점프 게임 — 도달할 수 있는 최대 인덱스를 추적합니다. 이 모든 문제는 교환 논증을 통한 증명으로 정당화할 수 있습니다.

# Fractional Knapsack: greedy works
def fractional_knapsack(items, capacity):
    # Sort by value/weight ratio descending
    items.sort(key=lambda x: x[1]/x[0], reverse=True)
    total = 0
    for weight, value in items:
        if capacity <= 0: break
        take = min(weight, capacity)
        total += take * (value / weight)
        capacity -= take
    return total

items = [(10, 60), (20, 100), (30, 120)]  # (weight, value)
print(fractional_knapsack(items, 50))  # 240.0

# 0/1 Knapsack: greedy FAILS
# Must use DP (can't take fractions)

탐욕법이 실패하는 경우: 반례

반례를 찾는 것은 탐욕법 가설을 반박하는 가장 빠른 방법입니다. 동전 [1, 3, 4]와 목표값 6을 사용하는 동전 교환에서 탐욕법(큰 동전부터)은 4를 선택한 다음 1+1을 선택하여 동전 3개를 사용합니다. DP는 3+3을 선택하여 동전 2개를 찾습니다. 0/1 배낭에서는 비율이 가장 높은 항목을 기준으로 탐욕적으로 선택하면 용량을 더 잘 채우는 조합을 놓칠 수 있습니다. 1분 안에 반례를 구성할 수 있다면 DP로 전환하십시오.

# Counterexample: coin change with non-standard coins
def greedy_coins(coins, amount):
    coins.sort(reverse=True)
    count = 0
    for c in coins:
        while amount >= c:
            amount -= c
            count += 1
    return count if amount == 0 else -1

def dp_coins(coins, amount):
    dp = [float('inf')] * (amount + 1)
    dp[0] = 0
    for a in range(1, amount + 1):
        for c in coins:
            if c <= a: dp[a] = min(dp[a], dp[a-c] + 1)
    return dp[amount] if dp[amount] < float('inf') else -1

coins, target = [1, 3, 4], 6
print('Greedy:', greedy_coins(coins[:], target))  # 3 (4+1+1)
print('DP:    ', dp_coins(coins, target))          # 2 (3+3)

비교 표: 탐욕법과 DP

핵심 차이를 나란히 비교해 보겠습니다. 시간 복잡도 — 탐욕법은 일반적으로 O(n log n)이며, 대부분 정렬이 지배합니다. DP는 O(n × 상태 수)입니다. 공간 복잡도 — 탐욕법은 보조 공간 O(1), DP는 O(상태 수)입니다. 정확성 — 탐욕법에는 증명이 필요하지만, DP는 상태와 점화식이 올바르면 항상 정확합니다. 적용 분야 — 탐욕법은 스케줄링, 신장 트리, 허프만 부호화에 사용하고, DP는 배낭, 수열 정렬, 음수 가중치가 있는 최단 경로에 사용합니다.

# Performance comparison
import time

def time_it(func, *args):
    start = time.time()
    result = func(*args)
    return result, time.time() - start

# Large coin change test
coins = [1, 5, 10, 25, 100]
amount = 10000

def dp_coins(coins, amount):
    dp = [float('inf')] * (amount + 1)
    dp[0] = 0
    for a in range(1, amount + 1):
        for c in coins:
            if c <= a: dp[a] = min(dp[a], dp[a-c]+1)
    return dp[amount]

result, elapsed = time_it(dp_coins, coins, amount)
print(f'DP coin change(amount={amount}): {result} coins in {elapsed:.4f}s')

의사 결정 체계

면접에서 사용할 의사 결정 순서도는 다음과 같습니다. (1) 교환 논증으로 탐욕 선택 속성을 증명할 수 있습니까? 그렇다면 → 탐욕법입니다. (2) 부분 문제가 겹칩니까(같은 상태에 여러 경로로 도달합니까)? 그렇다면 → DP입니다. (3) 모든 해의 개수를 묻거나 모든 해를 열거하라고 합니까? → DP 또는 백트래킹입니다. (4) 자연스러운 순서가 있는 하나의 최적값을 묻습니까? 탐욕법을 의심해 보십시오. (5) 확신이 서지 않으면 DP로 작성하십시오. 점화식이 올바르다면 더 느리더라도 항상 정확합니다.

# Decision questions to ask:
questions = [
    '1. Is there a natural ordering (by time, ratio, size)?',
    '2. Does making the greedy choice leave a smaller same-type problem?',
    '3. Can I construct a counterexample quickly?',
    '4. Are sub-problems reused across different choice sequences?',
    '5. Does the problem involve counting or listing (not just optimising)?',
]
for q in questions:
    print(q)

print()
print('Greedy signals: scheduling, spanning tree, Huffman, jump game')
print('DP signals: knapsack, edit distance, LCS, coin change (general)')

구간 문제: 탐욕법과 DP

구간 문제는 탐욕법과 DP로 나뉩니다. 겹치지 않는 구간(제거할 구간 최소화)에서는 종료 시각 기준으로 sort한 뒤 구간을 탐욕적으로 선택하면 탐욕법이 최적임을 증명할 수 있습니다. 가중 구간 스케줄링(전체 가중치 최대화)에서는 무거운 구간 하나가 가벼운 구간 여러 개와 겹칠 수 있으므로 모든 유효한 부분 집합을 비교해야 하며, 따라서 DP가 필요합니다. 구분 기준은 모든 구간의 가중치가 같은지(탐욕법), 아니면 가중치가 서로 다른지(DP)입니다.

# Non-overlapping intervals: greedy works
def erase_overlap_intervals(intervals):
    if not intervals: return 0
    intervals.sort(key=lambda x: x[1])
    count = 0
    last_end = float('-inf')
    for start, end in intervals:
        if start >= last_end:
            last_end = end  # keep this interval
        else:
            count += 1  # remove this interval
    return count

print(erase_overlap_intervals([[1,2],[2,3],[3,4],[1,3]]))  # 1
print(erase_overlap_intervals([[1,2],[1,2],[1,2]]))        # 2

문제의 신호 알아보기

문제 설명에서 자주 나타나는 신호는 다음과 같습니다. '최소 연산 횟수', '최대 이익', '최적의 선택' → 탐욕법일 수도 있고 DP일 수도 있으므로 겹침을 확인합니다. '방법의 개수를 세라' → 항상 DP입니다. '유효한 스케줄 하나를 찾아라' → 탐욕법일 수 있습니다. '모든 가능한 해' → 백트래킹입니다. '인접한 항목을 선택할 수 없다' → DP입니다(집 도둑 문제). '회의, 구간, 작업' → 탐욕법일 가능성이 높습니다. 신호를 알고리즘 계열에 연결하면 면접 문제를 더 빠르게 진단할 수 있습니다.

# Signal-to-algorithm mapping
signals = {
    'minimum steps/coins/operations': 'DP (unless trivially greedy)',
    'maximum profit/value with constraint': 'DP (knapsack family)',
    'count ways to reach/achieve': 'DP (always)',
    'all combinations/permutations': 'Backtracking',
    'schedule tasks within time': 'Greedy (sort by deadline/end)',
    'cannot pick adjacent': 'DP (house robber pattern)',
    'free to pick any subset': 'DP or Greedy (check overlap)',
    'interval merging/selecting': 'Greedy (sort by end time)',
}
for signal, algo in signals.items():
    print(f'{signal!r}: → {algo}')

탐욕법의 정확성 증명

탐욕 알고리즘이 올바름을 증명하려면 교환 논증을 사용하십시오. (1) 탐욕해 G와 첫 번째 선택이 다른 최적해 OPT가 존재한다고 가정합니다. (2) 목적 함수 값을 증가시키지 않고 OPT에 탐욕 선택을 넣도록 바꿀 수 있음을 보입니다. (3) 귀납법에 따라 탐욕해는 모든 최적해만큼 우수합니다. 면접에서는 완전한 증명까지 제시할 필요는 없지만, 교환 논증의 직관을 설명하면 깊이 있는 이해를 보여 줄 수 있습니다.

# Exchange argument demo: earliest-finish-time activity selection
# Suppose OPT starts with activity A (not earliest-ending)
# Let G = earliest-ending activity available
# A.end >= G.end (G ends earlier or same time)

# Swap A for G in OPT:
# - G.end <= A.end, so G does not conflict with anything A allowed after it
# - OPT remains valid with the same number of activities
# - Repeat: after swap, OPT begins with G, matching greedy first choice
# By induction, OPT can be transformed to match G activity by activity
# without losing activities → greedy is optimal

print('Exchange argument: any OPT can be modified to match Greedy without loss')
print('This proves Greedy >= OPT in objective value')

빠른 확인

이 단원에서 배운 자료 구조 및 알고리즘 — 코딩 면접 준비 개념을 이해했는지 test해 보십시오.

학습 내용 복습

이 단원에서는 다음을 배웠습니다. 탐욕 선택 속성이 성립하면 탐욕법은 올바르며, 이는 교환 논증으로 증명할 수 있습니다. 부분 문제가 겹치고(같은 부분 문제에 여러 경로로 도달하고) 하나의 탐욕 규칙으로 해결할 수 없다면 DP가 필요합니다. 또한 탐욕법 가설을 반박하는 가장 빠른 방법은 일반적이지 않은 입력으로 반례를 구성하는 것입니다. 다음에는 종료 시각 기준으로 sort하는 탐욕 접근법을 사용하여 구간 스케줄링과 구간 병합을 해결합니다.

자주 묻는 질문

“그리디와 DP: 각각 언제 사용할까” 강의는 무료인가요?

네 — “그리디와 DP: 각각 언제 사용할까” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Coding Interview Prep 강의 전체를 잠금 해제할 수 있습니다. Coding Interview Prep 강의에는 총 4개의 강의가 포함되어 있습니다.

“그리디와 DP: 각각 언제 사용할까”에서 뭘 배우나요?

그리디 선택 속성과 교환 논증을 사용해 그리디로 해결할 수 있는 문제와 DP가 필요한 문제의 특징을 구분합니다. 브라우저에서 직접 실행하는 실습 코드로 Coding Interview Prep을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

Coding Interview Prep을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 Coding Interview Prep은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 1번째 강의입니다.

“그리디와 DP: 각각 언제 사용할까” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 Coding Interview Prep 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 Coding Interview Prep 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. 그리디와 DP: 각각 언제 사용할까
  2. 구간 스케줄링과 병합
  3. 점프 게임 I과 II
  4. 작업 스케줄러와 주유소
← Coding Interview Prep(으)로 돌아가기