Divide and Conquer vs. Greedy Algorithm
“Divide and Conquer"와 “Greedy Algorithm"은 문제 해결을 위한 두 가지 중요한 알고리즘 패러다임이다.
분할 정복은 문제를 더 작은 하위 문제로 나누어 해결하는 체계적인 접근 방식인 반면, 탐욕 알고리즘은 각 단계에서 지역적 최적 선택을 통해 문제를 해결하는 직관적인 접근 방식이다.
분할 정복은 정확한 최적해를 보장하지만 구현이 복잡할 수 있고, 탐욕 알고리즘은 구현이 간단하고 효율적이지만 항상 최적해를 보장하지는 않는다. 문제의 특성을 잘 이해하고 적절한 알고리즘을 선택하는 것이 중요하다.
Divide and Conquer(분할 정복) 알고리즘
분할 정복은 복잡한 문제를 더 작고 관리하기 쉬운 하위 문제들로 나누어 해결하는 방법이다.
이 알고리즘은 다음과 같은 세 단계로 구성된다:
- 분할(Divide): 원래 문제를 더 작은 하위 문제들로 나눈다.
- 정복(Conquer): 각 하위 문제를 재귀적으로 해결한다.
- 결합(Combine): 하위 문제들의 해결책을 합쳐 원래 문제의 해결책을 만든다.
대규모 프로젝트 관리와 유사하다. 큰 프로젝트(문제)를 여러 팀에 나누어 할당(분할)하고, 각 팀이 자신의 과제를 해결(정복)한 다음, 모든 결과를 통합(결합)하여 전체 프로젝트를 완성한다.
작동 원리 예시: 병합 정렬(Merge Sort)
병합 정렬은 분할 정복의 대표적인 예:
|
|
이 알고리즘은 배열을 절반씩 나누어(분할) 각 부분을 재귀적으로 정렬(정복)한 다음, 정렬된 두 부분을 하나로 병합(결합)한다.
Greedy Algorithm(탐욕 알고리즘)
탐욕 알고리즘은 각 단계에서 현재 상황에서 가장 최적인 선택(지역적 최적해)을 선택하여 최종적으로 전체 문제의 최적해를 찾는 방법이다.
이 알고리즘은 다음과 같은 특징을 가진다:
- 지역적 최적 선택(Local Optimal Choice): 각 단계에서 현재 상황에서 최선의 선택을 한다.
- 탐욕적 선택 속성(Greedy Choice Property): 지역적 최적 선택이 전체 문제의 최적해로 이어진다.
- 최적 부분 구조(Optimal Substructure): 문제의 최적해가 하위 문제의 최적해를 포함한다.
등산을 할 때 매 순간 가장 가파른 경로를 선택하는 것과 유사하다(이를 ‘언덕 오르기’라고도 합니다). 각 지점에서 가장 높이 올라갈 수 있는 방향(지역적 최적)을 선택하여 산 정상(전체 최적)에 도달하려고 한다.
하지만 이 방법이 항상 정상에 도달하는 것을 보장하지는 않으며, 지역적 정상에 갇힐 수 있다.
작동 원리 예시: 거스름돈 문제
거스름돈 문제는 탐욕 알고리즘의 대표적인 예:
|
|
이 알고리즘은 매 단계에서 가장 큰 단위의 동전부터 사용하여(탐욕적 선택) 최소한의 동전으로 거스름돈을 만든다.
두 알고리즘의 핵심 차이점
문제 해결 접근 방식
- 분할 정복: 문제를 더 작은 하위 문제로 나누어 각각 해결한 후 결합한다.
- 탐욕 알고리즘: 각 단계에서 현재 상황에서 최선의 선택을 하여 문제를 해결한다.
결정 방식
- 분할 정복: 전체 문제를 고려하여 하위 문제로 분할하고, 모든 하위 문제의 해결책을 종합적으로 고려한다.
- 탐욕 알고리즘: 매 단계에서 지역적 최적 선택만을 고려하며, 이전 결정을 수정하지 않는다.
최적해 보장
- 분할 정복: 모든 경우를 고려하므로 일반적으로 최적해를 보장한다.
- 탐욕 알고리즘: 모든 문제에서 최적해를 보장하지는 않으며, 특정 조건(탐욕적 선택 속성, 최적 부분 구조)을 만족해야 한다.
예시로 보는 차이: 배낭 문제(Knapsack Problem)
배낭 문제는 두 알고리즘의 차이를 잘 보여주는 예이다:- 일반적인 배낭 문제: 여러 물건의 가치와 무게가 있을 때, 제한된 무게 내에서 최대 가치를 선택하는 문제
- 분할 가능한 배낭 문제: 물건을 부분적으로 선택할 수 있는 경우
분할 정복 접근: 모든 가능한 물건의 조합을 고려하여 최적해를 찾습니다. 이는 물건의 수가 많을 때 비효율적이다.
탐욕 알고리즘 접근: 단위 무게당 가치가 높은 물건부터 선택한다. 이 방법은 분할 가능한 배낭 문제에서는 최적해를 보장하지만, 일반 배낭 문제에서는 최적해를 보장하지 않는다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
# 분할 가능한 배낭 문제의 탐욕 알고리즘 해결법 def fractional_knapsack(capacity, weights, values): # 각 물건의 단위 무게당 가치 계산 items = [(values[i] / weights[i], weights[i], values[i]) for i in range(len(weights))] # 단위 무게당 가치를 기준으로 내림차순 정렬 items.sort(reverse=True) total_value = 0 for unit_value, weight, value in items: if capacity >= weight: # 물건 전체를 선택 total_value += value capacity -= weight else: # 물건의 일부만 선택 total_value += unit_value * capacity break return total_value
실제 적용 사례
- Divide and Conquer 적용 사례
- 퀵 정렬(Quick Sort): 배열을 피벗을 기준으로 분할하여 정렬
- 이진 검색(Binary Search): 정렬된 배열에서 중간점을 기준으로 검색 범위를 반으로 줄임
- 최대 부분 배열 문제(Maximum Subarray Problem): 배열을 분할하여 최대 합을 가진 부분 배열을 찾음
- 스트라센 알고리즘(Strassen’s Algorithm): 행렬 곱셈을 효율적으로 수행
- Greedy Algorithm 적용 사례
- 최소 신장 트리(Minimum Spanning Tree): 크루스칼(Kruskal), 프림(Prim) 알고리즘
- 다익스트라 알고리즘(Dijkstra’s Algorithm): 최단 경로 찾기
- 허프만 코딩(Huffman Coding): 데이터 압축
- 작업 스케줄링(Job Scheduling): 가장 짧은 작업부터 처리하는 방식
코드 비교: 동전 개수 최소화 문제
가능한 동전 종류가 주어졌을 때, 특정 금액을 만들기 위한 최소 동전 개수를 구하는 문제.
분할 정복 접근법 (실제로는 동적 계획법이 더 적합)
탐욕 알고리즘 접근법
여기서 주목할 점은 탐욕 알고리즘이 항상 최적해를 보장하지 않는다는 것.
예를 들어, 동전 종류가 [1, 3, 4]이고 금액이 6인 경우:- 탐욕 알고리즘: 4 + 1 + 1 = 3개의 동전 사용
- 최적해: 3 + 3 = 2개의 동전 사용
선택 가이드
- Divide and Conquer를 선택해야 할 때
- 문제가 동일한 구조의 더 작은 하위 문제로 자연스럽게 나눠질 때
- 문제의 크기가 크고 분할하여 해결할 수 있을 때
- 정확한 최적해가 필요할 때
- 병렬 처리가 가능하거나 필요할 때
- 전체 탐색 공간을 효율적으로 줄일 수 있을 때
- Greedy Algorithm을 선택해야 할 때
- 각 단계에서의 최선의 선택이 전체 문제의 최적해로 이어질 때
- 빠른 실행 시간이 중요할 때
- 문제의 크기가 크고 모든 경우를 고려하기 어려울 때
- 근사 해답이 허용될 때(최적이 아니더라도 충분히 좋은 해답)
- 문제가 최적 부분 구조와 탐욕적 선택 속성을 가질 때
- 문제 접근 방법
- 문제 분석: 문제의 구조와 특성을 파악합니다.
- 문제가 더 작은 하위 문제로 나눠질 수 있는지
- 지역적 최적 선택이 전체 최적해로 이어지는지
- 알고리즘 선택:
- 문제가 분할 정복의 구조를 가지고 있으면 → 분할 정복
- 문제가 탐욕적 선택 속성과 최적 부분 구조를 가지고 있으면 → 탐욕 알고리즘
- 검증:
- 분할 정복: 하위 문제들이 올바르게 정의되었는지, 결합 방법이 적절한지 확인
- 탐욕 알고리즘: 선택한 탐욕적 전략이 항상 최적해를 보장하는지 증명하거나 검증
- 문제 분석: 문제의 구조와 특성을 파악합니다.
응용 예제: 최소 신장 트리(MST) 구현
최소 신장 트리 문제는 주어진 그래프에서 모든 정점을 포함하면서 간선의 가중치 합이 최소인 트리를 찾는 문제.
이 문제는 탐욕 알고리즘인 크루스칼 알고리즘으로 효율적으로 해결할 수 있다.
|
|
이 크루스칼 알고리즘은 탐욕적 접근을 사용한다:
- 모든 간선을 가중치 순으로 정렬합니다.
- 가중치가 가장 작은 간선부터 선택합니다(탐욕적 선택).
- 사이클을 형성하지 않는 간선만 추가합니다.
두 알고리즘 비교
특성 | Divide and Conquer | Greedy Algorithm |
---|---|---|
기본 원리 | 문제를 작은 하위 문제로 분할하여 해결 | 각 단계에서 지역적 최적 선택을 통해 해결 |
접근 방식 | 재귀적, 하향식(top-down) | 반복적, 순차적, 상향식(bottom-up) |
문제 해결 순서 | 분할 → 정복 → 결합 | 선택 → 실행 → 다음 단계로 이동 |
최적해 보장 | 일반적으로 보장 | 조건을 만족할 때만 보장(탐욕적 선택 속성, 최적 부분 구조) |
결정 철회 가능성 | 전체 결과를 고려하므로 가능 | 한번 내린 결정은 번복하지 않음 |
적용 조건 | 문제가 분할 가능할 때 | 지역적 최적해가 전체 최적해로 이어질 때 |
시간 복잡도 | 일반적으로 O(n log n) (예: 병합 정렬) | 일반적으로 O(n) 또는 O(n log n) |
구현 복잡성 | 중간~높음 (재귀 구현) | 낮음~중간 (직관적인 구현) |
메모리 사용 | 재귀 호출 스택으로 인해 높을 수 있음 | 일반적으로 낮음 |
대표 알고리즘 | 병합 정렬, 퀵 정렬, 이진 검색 | 다익스트라, 크루스칼, 허프만 코딩 |
재귀 사용 | 필수적 | 선택적(주로 반복문 사용) |
적합한 문제 유형 | 정렬, 검색, 행렬 연산 등 | 최적화 문제, 스케줄링, 경로 찾기 등 |
결정 기준 | 문제의 구조에 따른 분할 | 휴리스틱이나 평가 함수(가중치, 비용 등) |
장점 | 복잡한 문제를 체계적으로 해결, 병렬 처리 가능 | 구현이 간단하고 효율적, 빠른 실행 시간 |
단점 | 재귀로 인한 오버헤드, 구현 복잡성 | 항상 최적해를 보장하지 않음 |
기억해야 할 핵심 포인트 | “분할, 정복, 결합” | “현재 최선의 선택” |