1. 정의
그리디(Greedy)는 매 단계에서 국소적으로 최선이라 판단되는 선택을 하고, 그 선택을 취소하지 않은 채 끝까지 밀어붙여 전체 해를 구성하는 방법이다. 장점은 압도적인 단순함과 속도, 단점은 항상 정답을 보장하지 않는다는 점.
2. 성립 조건
- 선택 속성(Greedy choice property): 매 단계의 최선 선택이 전체 최적해의 일부로 확장 가능해야 한다.
- 최적 부분 구조(Optimal substructure): 전체 최적해가 부분 문제의 최적해들로 구성되어야 한다.
3. 전형적 패턴
- 정렬 + 선형 스캔: 기준을 정의하고 정렬한 뒤, 앞/뒤에서 하나씩 선택.
- 우선순위 큐(힙): 매 순간 가장 좋은 후보를 즉시 꺼내기.
- 몫/나머지 반복: 거스름돈처럼, 남은 값으로 몫/나머지를 갱신.
- 인터벌 선택: 끝나는 시간, 시작 시간 등 핵심 기준 하나를 고정하고 탐욕 선택.
4. 잘 통하는 대표 문제
① 동전 거스름(11047 유형)
coins = [500, 100, 50, 10]
amount = 4720
cnt = 0
for c in coins:
cnt += amount // c
amount %= c
print(cnt)
② 활동 선택(회의실 배정)
끝나는 시간이 가장 빠른 활동부터 채택하면, 전체 선택 가능한 활동 수가 최대가 된다. (끝시간 오름차순 정렬 → 겹치지 않으면 채택)
③ 최소 신장 트리(MST)
- Kruskal: 간선을 가중치 오름차순으로 정렬, 사이클이 생기지 않으면 채택(Union-Find).
- Prim: 트리에 인접한 간선 중 가장 가벼운 간선을 확장(우선순위 큐).
④ 허프만 코딩
가장 빈도가 낮은 두 노드를 반복적으로 합치는 탐욕 선택으로 최적의 접두부 코드가 만들어진다.
5. 그리디가 깨지는 반례
동전 = [500, 400, 100], 금액 = 800
그리디: 500 + 100 + 100 + 100 = 4개
최적해: 400 + 400 = 2개 ← 그리디 실패
동전 = [1, 3, 4], 금액 = 6
그리디: 4 + 1 + 1 = 3개
최적해: 3 + 3 = 2개
6. 증명 스케치: 왜 맞는가
- 교환 논법(Exchange argument): 임의의 최적해가 있다고 가정하고, 가장 앞의 선택을 그리디 선택으로 바꿔도 품질이 나빠지지 않음을 보인다. 이를 반복하면 그리디 해도 최적해임이 성립.
- 귀납/단조성: 한 단계의 최선 선택 후 남은 부분 문제에서도 같은 구조가 반복됨을 보인다.
7. 구현 팁 & 복잡도
- 정렬은 보통 O(N log N). 이후는 선형 스캔 O(N).
- 힙을 쓰면 매 선택 O(log N). (Prim, 허프만 등)
- 불변식을 명확히: “지금까지의 선택은 항상 유효하다”를 유지.
- 경계/예외: 동등 값, 빈 입력, 0/음수, 오버플로우 주의.
8. DP / 백트래킹과 비교
- 빠름, 구현 간단
- 정답 보장은 문제 성질에 의존
- 전역 최적 보장
- 상태 정의/점화식 필요, 메모리 사용↑
- 정확하지만 느림
- 가지치기로 평균 성능 개선
9. 코드 스니펫
① 동전 거스름 (정규 화폐)
def greedy_change(coins, amount):
cnt = 0
for c in sorted(coins, reverse=True):
if c <= 0:
continue
cnt += amount // c
amount %= c
return cnt
② 활동 선택
def activity_selection(intervals):
intervals.sort(key=lambda x: x[1]) # 끝시간 기준
res = []
last_end = -float('inf')
for s,e in intervals:
if s >= last_end:
res.append((s,e))
last_end = e
return res
③ Kruskal (개요)
def kruskal(n, edges):
parent = list(range(n))
def find(x):
while x != parent[x]:
parent[x] = parent[parent[x]]
x = parent[x]
return x
def union(a,b):
ra, rb = find(a), find(b)
if ra == rb: return False
parent[rb] = ra
return True
mst = []
for w,u,v in sorted(edges):
if union(u,v):
mst.append((w,u,v))
return mst
10. 체크리스트
- 탐욕 기준이 항상 맞는지 증명(교환 논법/반례 탐색)했는가?
- 정렬/우선순위 기준이 문제의 목표와 정확히 일치하는가?
- “현재 남은 값”을 기준으로 갱신하고 있는가? (거스름: 몫/나머지)
- 동률 처리 규칙이 필요한가? (예: 종료시간 동일 시 시작시간 순)
- 반례 케이스(비정규 화폐 등)에 대한 대체 접근(DP)을 준비했는가?
'WEEK04 > Algorithm' 카테고리의 다른 글
| [Week04] Dynamic Progrmming, Linked-List (0) | 2025.09.26 |
|---|---|
| [Week03] 최소 신장 트리 (0) | 2025.09.23 |