[알고리즘] Greedy algorithm
Greedy 알고리즘
: 목적을 달성하기 위해 현재의 정보만으로 각 단계에서 가장 좋은 선택을 반복해서 답을 구성하는 방법
- 탐욕 선택 속성
- 최적 부분 구조
위의 특성을 가지는 문제들을 해결하는데 강점이 있음
즉, 한 번의 선택이 다음 선택에는 전혀 무관한 값이어야 하며 매 수간의 최적해가 문제에 대한 최적해여야 한다는 의미
예시 1️⃣ : 최소 개수의 동전
Q. 1원, 10원, 50원, 100원 동전이 있을 때, 최소 개수의 동전으로 371원을 거슬러 주고 싶다. 어떤 동전들을 몇 개씩 사용해야 할까? |
이 질문의 답으로 우리는
100 * 3 + 50 * 1 + 10 * 2 + 1 * 1 = 371
위와 같은 계산으로 3 + 1 + 2 + 1 = 7 개의 동전들로 최소 개수가 이루어짐을 알았을 것이다.
이는 어떤 방식으로 생각된거냐면!
- 371원을 거슬러 줄 수 있는 최대 금액의 동전으로 최대한 많이 거슬러야 최소가 될 거 같다. 즉, 100 * 3 = 300원을 먼저 거슬러 주자
- 남은 71원을 100원 다음으로 큰 50원으로 거슬러준다. 50 * 1 = 50원을 거슬러 주자
- 남은 21원에 대해선 10원 * 2개 + 1원 * 2개 로 거슬러 준다
만약 1원, 5원, 7원이 동전 단위인 경우 10원을 거슬러 주고 싶다면?
앞의 방식대로 해결하면 7 * 1 + 1 * 3 으로 총 4개의 동전으로 거슬러 줄 수 있다.
하지만 5 * 2 = 10 이므로 2 개의 동전으로 거슬러 주는 것이 최소이다.
이 문제에 대해선 앞의 방식이 최고의 방법이 아니다.
왜냐하면 1원이 10원이 될 순 있지만 5원이 7원이 될 수는 없기 때문이다.
즉, 작은 단위의 배수가 큰 단위와 일치하지 않아서 이런 방식에 대해선 greedy 알고리즘을 사용하는 것이 옳지는 않다.
예시 2️⃣ : 강의실 배정
Q. 강의실이 하나 뿐인데, 이 강의실에서 쓰고 싶은 강의가 많다. 강의가 서로 겹치지 않게 최대한 많은 강의를 배정하라. 입력 : n 개의 강의가 주어진다. 강의 i 번은 S[i]에 시작해서 F[i]에 끝난다. 출력: 강의실의 개수 |
greedy 기준을 생각해보자 ( 각 그림은 반례를 표현한 것 )
수업시간이 가장 짧은 강의부터 선택 | ![]() |
가장 적게 겹치는 강의부터 선택 | |
가장 빨리 시작하는 강의부터 선택 | ![]() |
가장 빨리 끝나는 강의부터 선택 | 요게 정답임ㅎ.ㅎ |
정답인 기준에 의해 수업을 배정해보면 다음과 같은 과정을 따른다!
- F의 값을 기준으로 S와 F 모두 오름차순으로 정렬( O(nlogn)의 시간복잡도를 가짐 )
- L 이라는 배열을 만듬, 이 배열에는 뽑힌 강의의 번호(i)가 들어갈 거임. 이미 F를 오름차순으로 정렬했으므로 S[0]으로 시작하고 F[0]으로 끝나는 강의가 가장 빨리 끝나는 강의 즉 0번 강의는 무조건 들어감
- for문을 돌면서 강의가 겹치지 않도록 L 배열에 넣고 가장 최근에 뽑힌 강의의 번호를 k 에 저장
Pseudo 코드
L = [0]
k = 0
for i in range(n):
if S[i] >= F[k] :
L.append(i)
k = i
관련 문제
https://www.acmicpc.net/problem/11000
11000번: 강의실 배정
첫 번째 줄에 N이 주어진다. (1 ≤ N ≤ 200,000) 이후 N개의 줄에 Si, Ti가 주어진다. (0 ≤ Si < Ti ≤ 109)
www.acmicpc.net
예시 3️⃣: Huffman Coding 문제
허프만 코드란?
데이터를 효율적으로 압축하는데 사용하는 방법으로 허므만 알고리즘에 의해 생성된 최적 이진코드를 말한다.
압축하고자 하는 문자열에서 자주 등장하는 문자는 짧은 비트로 표현하고 거의 등장하니 않는 문자는 긴 비트로 표현한다(가변 길이 코드)
즉, 문자의 빈도수를 이용함!
예를들어
문자 | 빈도수 | 고정길이코드 | 가변길이코드 |
a | 43 | 000 | 0 |
b | 13 | 001 | 101 |
c | 12 | 010 | 100 |
d | 16 | 011 | 111 |
e | 9 | 100 | 1101 |
f | 7 | 101 | 1100 |
여기서 고정길이 코드는 6개의 문자를 구분하기 위해 3bit를 필요로 한다.
(2^2 < 6 < 2^3)
총 빈도수가 100이므로 고정길이 코드로 암호화 한 경우엔 100 * 3 = 300bit가 필요하고
가변길이 코드로 암호화 한 경우엔 1* 43 + 3 * (13 + 12 + 16) + 4 * (9 + 7) = 230bit가 필요하다
가변길이코드가 훨씬 적은 메모리를 사용함을 알 수 있음
모호성이 없는 코드 체계는 항상 prefix-free 코드여야 한다.
prefix-free 코드란? 한 코드가 다른 코드의 prefix에나타나지 않는 코드 체계를 말한다 예를 들어 'a' = 0 / 'b' = 11 / 'c' = 011 라고 하자 그럼 011이란 이진코드가 주어질 때 이것이 'ab'인지 'c'인지 알 수 없다 그래서 주어진 문자열을위한 최적 이진트리를 구축하기 위해서는 prefix code로 구현되어야 한다. |
greedy 기준
- 빈도수가 낮은 문자를 먼저 짝을 지어 트리를 점진적으로 구성
- 먼저 짝을 지어 구성하므로 빈도수가 낮은 문자가 루트노드에서 멀어지게 되는 효과가 있다
위의 표를 예시로 이진트리를 구성하면 다음과 같다.
이는 heap의 자료구조를 통해 코드를 작성할 수 있다
Pseudo code
H = []
for x in range(n):
heappush(H, (f[x], 'x'))
while len(H) > 1:
a = heappop(H) # 가장 작은 빈도수 item
b = heappop(H) # 두 번째로 작은 빈도수 item
heappush(H, a.f + b.f, '(a.x b.x')
tree_string = heappop(H)[1]