출시·고도화 중
탐욕 알고리즘 안내서 · 1/6
탐욕 알고리즘(greedy algorithm)은 문제를 여러 단계의 선택으로 나누고, 각 단계에서 지금 가장 좋아 보이는 것을 고른 뒤 그 선택을 다시 돌아보지 않는 설계 방법입니다. 모든 경우를 따져 보는 완전 탐색이나 부분 문제의 답을 표에 모아 두는 동적 계획법과 달리, 탐욕 알고리즘은 한 길만 따라갑니다. 그래서 코드가 짧고 빠르지만, 그 한 길이 정말 최적의 답으로 이어진다는 것을 따로 증명해야 합니다.
| 용어 | 뜻 |
|---|---|
| 탐욕 선택 | 현재 상태에서 정해진 기준으로 가장 좋아 보이는 후보를 고르는 것 |
| 탐욕 선택 속성 | 첫 탐욕 선택을 포함하는 최적해가 항상 존재한다는 성질 |
| 최적 부분 구조 | 첫 선택을 하고 남은 문제의 최적해가 전체 최적해의 일부가 되는 성질 |
| 교환 논법 | 어떤 최적해를 탐욕 해 쪽으로 한 원소씩 바꿔도 손해가 없음을 보이는 증명 방법 |
| 앞서 나가기 논법 | 매 단계에서 탐욕 해가 다른 어떤 해보다 뒤처지지 않음을 보이는 증명 방법 |
탐욕 알고리즘이 옳으려면 탐욕 선택 속성과 최적 부분 구조가 함께 성립해야 합니다. 앞의 것은 "첫 선택이 틀리지 않는다"는 보장이고, 뒤의 것은 "첫 선택 뒤에 남은 문제도 같은 방식으로 풀면 된다"는 보장입니다. 두 성질이 있으면 귀납적으로 모든 선택이 옳다는 결론이 나옵니다.
탐욕 알고리즘을 증명할 때 가장 많이 쓰는 방법이 교환 논법입니다. 흐름은 늘 비슷합니다.
예를 들어 겹치지 않는 회의를 가장 많이 고르는 문제에서는 "가장 일찍 끝나는 회의"를 먼저 고릅니다. 어떤 최적해의 첫 회의를 가장 일찍 끝나는 회의로 바꿔도, 새 회의는 원래 회의보다 늦게 끝나지 않으므로 뒤의 회의들과 겹치지 않습니다. 회의 개수는 그대로이니 이 교환은 손해가 없습니다.
동전으로 거스름돈을 줄 때 큰 동전부터 최대한 쓰는 방법은 가장 익숙한 탐욕 알고리즘입니다. 한국 동전(10, 50, 100, 500원)처럼 잘 설계된 동전 체계에서는 이 방법이 항상 동전 수를 최소로 만듭니다.
def greedy_coins(coins, amount):
"""큰 동전부터 최대한 쓴다. 정확히 맞출 수 없으면 None."""
count = 0
for coin in sorted(coins, reverse=True):
take, amount = divmod(amount, coin)
count += take
return count if amount == 0 else None
print(greedy_coins([10, 50, 100, 500], 780)) # 7 (500 + 100 * 2 + 50 + 10 * 3)동전이 1, 3, 4원이고 6원을 거슬러 줘야 한다고 해 봅시다. 탐욕은 4원을 먼저 쓰고 1원 두 개를 더해 동전 3개를 씁니다. 그러나 3원 두 개면 동전 2개로 충분합니다. 첫 선택(4원)이 최적해에 들어가지 않으므로 탐욕 선택 속성이 깨진 것입니다. 이런 경우에는 동적 계획법으로 모든 금액의 최소 동전 수를 계산해야 합니다.
def min_coins(coins, amount):
"""동적 계획법: best[a] = a원을 만드는 최소 동전 수."""
INF = float("inf")
best = [0] + [INF] * amount
for a in range(1, amount + 1):
for c in coins:
if c <= a and best[a - c] + 1 < best[a]:
best[a] = best[a - c] + 1
return best[amount] if best[amount] != INF else None
print(greedy_coins([1, 3, 4], 6), min_coins([1, 3, 4], 6)) # 3 2증명을 세우기 전에, 또는 증명이 맞는지 의심될 때는 작은 입력에서 탐욕 해와 정답(완전 탐색이나 동적 계획법)을 비교해 보는 것이 가장 빠른 확인 방법입니다. 반례가 하나라도 나오면 그 탐욕 기준은 틀린 것입니다.
def find_counterexample(coins, limit=100):
for amount in range(1, limit + 1):
g, d = greedy_coins(coins, amount), min_coins(coins, amount)
if g != d:
return amount, g, d
return None
print(find_counterexample([1, 3, 4])) # (6, 3, 2)
print(find_counterexample([10, 50, 100, 500])) # None반례가 나오지 않았다고 해서 증명이 된 것은 아니지만, 틀린 기준을 일찍 버리는 데는 매우 효과적입니다.
댓글 0개
로그인 · 로그인하면 댓글을 남길 수 있습니다.
첫 댓글을 남겨 보세요.