- 그리디 알고리즘은 최적화 문제를 해결하는 알고리즘
* 최적화 문제: 가능한 해들 중에서 가장 좋은 해(최소 비용, 최대 이익 등..)를 찾는 문제
- 욕심쟁이 방법, 탐욕적 방법, 탐욕 알고리즘 등으로 불림
- 근시안적 선택
: 입력 데이터 간 관계 고려x 수행 과정에서 순간순간 최솟값 또는 최댓값 가진 데이터 선택
- 일단 한 번 선택하면 -> 절대로 번복하지 x
=> 대부분의 그리디 알고리즘들은 매우 단순, 제한적인 문제만이 그리디 알고리즘으로 해결 가능
동전 거스름돈 문제
남은 액수를 초과하지 않는 조건 하에 욕심내어 가장 큰 액면의 동전을 취하는 것
CoinChange
입력: 거스름돈 액수 W
출력: 거스름동 액수에 대한 최소 동전 수
change=W, n500=n100=n50=n10=n1=0
while(change>=500)
change=change-500, n500++ //500원 동전 수 1 증가
while(change>=100)
change=change-100, n100++ //100원 동전 수 1 증가
while(change>=50)
change=change-50, n50++ //50원 동전 수 1 증가
while(change>=10)
change=change-10, n10++ //10원 동전 수 1 증가
while(change>=1)
change=change-1, n1++ //1원 동전 수 1 증가
return (n500+n100+n50+n10+n1) //총 동전 수 리턴
def coin_change(don):
coin=[500,100,50,10,5,1]
count=0
for i in range(len(coin)):
count don // coin[i] #동전 수
don=don%coin[i] #남은 돈
if count!=0:
print('{:3}원짜리 동전: {:2}개'.format(coin[i], count))
문제점
한국은행에서 160원 동전을 추가로 발행한다면, CoinChange 알고리즘이 항상 최소 동전 수를 계산할 수 있을까?
- 거스름돈이 200원이라면, CoinChange 알고리즘은 160원 동전 1개와 10원 동전 4개로서 총 5개를 리턴
- 200원에 대한 최소 동전 수는 100원짜리 동전 2개
=> 썼을 때보다 안 썼을 때 동전 개수가 적음
최소 신장 트리(Minimum Spanning Tree)
주어진 가중치 그래프에서 사이클이 없이 모든 점들을 연결시킨 트리들 중 간선들의 가중치 합이 최소인 트리
<트리가 되기 위한 조건>
1. n개의 vertax를 n-1개의 edge가 연결하고 있으며(∵ 그래야 모든 점들이 연결될 수 있기 때문)
2. 점들 간 cycle 없을 때(∵ edge가 n-1보다 크면 반드시 cycle 만들어짐)

<최소 신장 트리>
다음 중 최소 신장 트리는 1번. 왜냐.
=> 3번 한 점이 빠져서 안 되고, 2번은 가중치 합이 커서 안 됨

- Kruskal 알고리즘: 정렬 사용
- Prim 알고리즘: 정렬 사용하지 않고 임의의 점 하나를 선택한 후 그걸 기준으로 추가시켜나감
크러스컬(Kruskal) 알고리즘
: 가중치가 가장 작은 간선이 사이클을 만들지 않을 때만 욕심내어 그 간선 추가
쉽게 말해, 남은 것 중에 최소 가중치 간선부터 추가

<알고리즘>
KruskalMST(G)
입력: 가중치 그래프 G=(V,E), |V|=n, |E|=m //입력은 1개의 연결 성분으로 된 가중치 그래프
출력: 최소 신장 트리 T
1. 가중치의 오름차순으로 간선들 정렬: L = 정렬된 간선 리스트 //** 포인트1
2. T=∅ // 트리 T를 초기화
3. while(T의 간선 수 < n-1) //트리의 간선 수가 n-1이 되면 더 이상 안 해도 됨
L에서 가장 작은 가중치를 가진 간선 e를 가져오고, e를 L에서 제거
if(간선 e가 T에 추가되어 사이클을 만들지 않으면) //**포인트2(사이클 검사)
e를 T에 추가
else //e가 T에 추가되어 사이클이 생기는 경우
e를 버린다.
return 트리 T //T는 최소 신장 트리
<시간복잡도>
(Line 1) 간선 정렬하는 데 O(mlogm)
* m = 입력 그래프에 있는 간선 수
(Line 2) 초기화하는 것이므로 O(1)
(Line 3~8) 사이클 검사 -**point**
while 루프는 최대 m번 수행
while 루프 내에서는 사이클 검사 O(1)
==> O(mlogm)
def find(u): # 경로 연산
if u != p[u]: # u가 u자신의 부모가 아니면
p[u] = find(p[u]) #경로 압축
return p[u]
def union(u,v): # 부모 설정
root1 = find(u)
root2 = find(v)
p[root2] = root1
weights = [ .... ] #정렬된 간선들
mst = [] #최종 트리
N = 정점 수
p = [] #부모 정점 정보 for 사이클 검사(초기값은 자기 자신)
tree_edges = 0
mst_cost = 0
while True:
if tree_edges == N-1:
break
u,v,wt = weights.pop(0) # 다음 최소 가중치 간선 가져오기
if find(u) != find(v): # 사이클 검사(중요)
union(u, v) # 부모 설정
mst.append((u,v)) #트리에 (u,v) 추가
mst_cost += wt


1. b-c(1) 선택
find(b): p[b]=b(초기화 상태)이므로, 그냥 b를 리턴
find(c): p[c]=c(초기화 상태)이므로, 그냥 c를 리턴
find(b)!=find(c)이므로
union(b,c): b가 c의 부모가 됨(p[c]=b)

2. c-f(1) 선택
find(c): p[c]=b이므로, p[c]=find(b)=b 리턴
find(f): p[f]=f이므로, 그냥 f를 리턴
find(c)!=find(f)이므로
union(c,f): find(c)=b가 f의 부모가 됨(p[f]=b)

3. a-d(2) 선택
find(a): p[a]=a이므로, a 리턴
find(d): p[d]=d이므로, d 리턴
find(a)!=find(d)이므로
union(a,d): find(a)=a가 d의 부모가 됨(p[d]=a)

4. b-f(2) 선택
find(b): p[b]=b이므로, 그냥 b를 리턴
find(f): p[f]=b이므로, p[f]=find(p[f])=find(b)=b 리턴
=> find(b)=find(f)이므로 선택x(cycle 발생)
5. d-e(3) 선택
find(d): p[d]=a이므로, p[d]=find(p[d])=find(a)=a 리턴
find(e): p[e]=e이므로 e 리턴
=> find(d)!=find(e)이므로
union(d,e): find(d)=a가 e의 부모가 됨(p[e]=a)

6. a-e(4) 선택
find(a): p[a]=a이므로 a 리턴
find(e): p[e]=a이므로 p[e]=find(p[e])=find(a)=a 리턴
=> find(a)=find(e)이므로 선택x(cycle 발생)
7. b-d(4) 선택
find(b): p[b]=b이므로 b 리턴
find(d): p[d]=a이므로 p[a]=find(p[a])=find(a)=a 리턴
=> find(b)!=find(d)이므로
union(b,d): find(b)=b가 find(d)=a의 부모가 됨(p[a]=b)

코드 없이 다음과 같이 풀면 된다.

프림 알고리즘
: 임의의 점 하나 선택 -> (n-1)개의 간선 하나씩 추가
쉽게 말해, 임의의 점 하나를 먼저 선택하고, 두 점과 연결된 모든 간선들 중 최소 가중치 가진 점부터 추가
(Line 1)
임의의 점 c 선택, D[c]=0으로 초기화

(Line 2~6)
- 시작점 c와 간선으로 연결된 각 점 v에 대해, D[v]를 각 간선의 가중치로 초기화
- 나머지 각 점 v에 대해, D[v]는 ∞로 초기화

(Line 7)
T={c}로 초기화
(Line 8~9)
1. min{∞,1,∞,∞,1}=1
=> T에 가장 가까운 점 b를 추가

=> b에 연결된 a와 d의 D[a]와 D[d] 갱신

2. min{3,4,∞,1}=1
=> T에 가장 가까운 점 f를 추가

=> f에 연결된 e의 D[e] 갱신
3. min{3,4,9}=3
=> a를 T에 추가

=> a에 연결된 d, e의 D[d]와 D[e] 갱신

4. min{4,2}=2
=> d를 T에 추가

=> 갱신x

5. min{4}=4
=> e를 T에 추가

=> MST 완성

PrimMST(G)
입력: 가중치 그래프 G=(V,E), |V|=n, |E|=m
출력: 최소 신장 트리 T
1. G에서 임의의 점 p를 시작점으로 선택 D[p]=0
//D[v]는 T에 있는 u와 v를 연결하는 간선의 최소 가중치를 저장하기 위한 원소
2. for (점 p가 아닌 각 점 v에 대하여) // 배열 D의 초기화
if (간선 (p,v)가 그래프에 있으면)
D[v] = 간선 (p,v)의 가중치
else
D[v] = ∞
T={p} //초기에 트리 T는 점 p만을 가진다.
while (T에 있는 점의 수 < n) {
T에 속하지 않은 각 점 v에 대하여,
D[v]가 최소인 점 vmin과 연결된 간선 (u, vmin)을 T에 추가,
여기서 u는 T에 속한 점이고, 점 vmin도 T에 추가
for(T에 속하지 않은 각 점 w에 대해서)
if(간선 (vmin, w)의 가중치 < D[w]) //D[w]를 갱신
}
<시간 복잡도>
1. while-루프가 (n-1)회 반복
2. T에 속하지 않은 각 점 v에 대해, D[v]가 최소인 점 vmin을 찾는 데 O(n) 소요(∵ 배열의 크기=n)
=> (n-1)*O(n)=O(n^2)
만약, 단순 배열이 아니라 최소 힙을 사용해 vmin 찾는다면,
* Binary Search Tree(이진탐색트리)는 Complete binary tree와 달리 균형이 잡혀 있지 않다.
* max/min heap이 있는데, max heap은 큰 숫자부터 위에서 아래로(형제관계는 상관 x)
min heap은 작은 숫자부터 위에서 아래로(형제관계는 상관 x)
=> priority queue 형태로 루트부터 꺼냄 => 꺼낸 후에는 complete binary tree 형태로 재배치

=> 루트 꺼내면 오른쪽 아래 요소를 루트로 옮겨야 하므로 O(logn)
간선 수를 m이라고 하면 O(mlogn)
간선 수가 O(n)이면 O(nlogn)
<응용>
: 최소 비용으로 선로 또는 파이프 네트워크를 설치하는 데 활용
(인터넷 광 케이블 선로, 케이블 TV 선로, 전화 선로, 송유관로, 가스관로, 배수로 등)

최단 경로 찾기
: 주어진 가중치 그래프에서 어느 한 출발점에서 또 다른 도착점까지의 최단 경로 찾기
다익스트라(Dijkstra) 알고리즘
: 주어진 출발점에서 시작
: 출발점으로부터의 최단 거리가 확정되지 않은 점들 중 출발점으로부터 가장 가까운 점 추가 -> 그 점의 최단 거리 확정
Prim | Dijkstra |
최단 거리가 확정되지 않은 점들 중 출발점으로부터 가장 가까운 점부터 추가해나감 | |
추가된 점까지의 거리만 계산 | 누적 거리 계산 |
<알고리즘>
ShortestPath(G,s)
입력: 가중치 그래프 G=(V,E), |V|=n, |E|=m
출력: 출발점 s로부터 (n-1)개의 점까지 각각 최단 거리를 저장한 배열 D
1. 배열 D를 로 초기화. 단, D[s]=0으로 초기화
//배열 D[v]에는 출발점 s로부터 점 v까지의 거리를 저장
while(s로부터의 최단 거리가 확정되지 않은 점이 있으면)
현재까지 최단 거리가 확정되지 않은 각 점 v에 대해서
최소의 D[v]의 값을 가진 점 vmin을 선택하고,
s로부터 점 vmin까지의 최단 거리 D[vmin]을 확정
s로부터 현재보다 짧은 거리로 점 vmin을 통해 우회 가능한 각 점 w에 대해서 D[w]를 갱신 //이것이 Prim 알고리즘과 다른 점
//간선 완화
return D
간선 완화하는 9~13번째줄 외우자.
for k in range(N)
m = -1
min_value = sys.maxsize
for j in range(N): #방문 안 된 정점들의 D 원소들 중 최솟값을 가진 정점 m 찾기
if not visited[j] and D[j] < min_value:
min_value = D[j]
m = j
visited[m] = True
for w, wt in g[m]: # m에 인접한 방문 안 된 각 정점의 D의 원소 갱신
if not visited[w]:
if D[m]+wt < D[w]:
D[w] = D[m] + wt # 간선 완화
previous[w] = m
<step1>
: 서울 확정
=> 서울과 이웃한 원주, 천안 간선 완화
* 간선 완화란 그냥 아래처럼 표에 거리 채워넣는 것을 말함

<step2>
: 천안 확정
=> 논산, 대전 간선 완화

<step3>
: 원주 확정
=> 강릉, 대구 간선 완화

<step4>
: 논산 확정
=> 대전 간선 완화

<step5>
: 대전 확정
=> 간선 완화x

<step6>
: 대구 확정
=> 포항, 부산 간선 완화

<step7>
: 광주 확정
=> 간선 완화 x

<step8>
: 부산 확정
=> 포항 간선 완화

<step9>
: 강릉 확정

<step10>
: 포항 확정

<시간 복잡도>
- while 루프가 (n-1)회 반복되고, 1회 반복될 때
배열에서 최솟값 찾음 => 최소의 D[v]를 가진 점 vmin을 찾는 데 O(n) 시간 소요
vmin에 연결된 점의 수가 최대 (n-1)개 => D[w]를 갱신하는 데 O(n) 시간 소요
(n-1)*(O(n)+O(n))=O(n^2)
배열 대신 최소 힙을 사용하면 => O(mlogn)
간선 수가 O(n)이면 => O(nlogn)
Kruskal | Prim | Dijkstra | |
시간복잡도 | O(mlogm) | 배열 사용 시 O(n^2) 힙 사용 시 O(mlogn) |
배열 사용 시 O(n^2) 힙 사용 시 O(mlogn) |
'학교 강의 > 컴퓨터알고리즘' 카테고리의 다른 글
[Chapter5] 동적 계획 알고리즘 (2) | 2025.04.28 |
---|---|
[Chapter4] 그리디 알고리즘(2) (1) | 2025.04.27 |
선택 문제 (0) | 2025.04.24 |
퀵 정렬 (0) | 2025.04.24 |
합병 정렬 (0) | 2025.04.23 |