욕심쟁이 방식은 말 그대로 (탐욕적으로) 가장 저렴한 비용이 들거나 효율적인 방법을 찾아가는 알고리즘입니다.
다익스트라Djikstra 알고리즘은 네덜란드 공학자
에츠허르 데이크스트라가
고안한 최단 경로 찾기 방법입니다. 약혼상대와의 쇼핑 중 쉬면서 20분만에 아이디어를 떠올려 냈다고 하네요?
이것은 최단 경로를 찾을 시 플로이드 알고리즘과 함께 많이 사용됩니다. (참고로 플로이드는 최적 부분 구조를 활용해 경유노드 k를 고려하여 모든 정점 간의 최단 경로를 찾아갑니다.)
가중 방향 그래프에서 특정 정점으로부터 다른 모든 정점까지의 최단 경로를 구하는 이 알고리즘의 아이디어는 욕심쟁이 방법으로 목적지까지의 최단거리를 찾아가는 것입니다. 직접 도달하거나 다른 노드를 경유해보면서 가장 가까운 거리를 찾으므로 그리디로 볼 수도 있지만, 이전 방문 노드까지의 최단거리로부터 다음 방문 노드에서의 최단거리들을 구해내므로 최적 부분 구조도 갖는다고 볼 수 있습니다.
다시 말해 다익스트라 알고리즘은 출발 노드로부터 가장 가까운(비용이 적게 드는, 혹은 효율적인) 노드들을 차례로 방문해가며 다른 모든 노드들에 대한 간선 가중치 합을 비교해 최단거리를 갱신합니다. 방문하는 노드는 인접한 다른 정점까지 가는 데 경유지가 되기 때문에 간선 가중치의 합이 기존의 해당 경로까지의 거리보다 가까운지 따져볼 수 있습니다. 가깝다면 간선 가중치의 합으로 최단거리가 갱신됩니다.
이 때 음수의 가중치를 갖는 간선은 없다고 가정을 하는데, 현실에서 길을 찾아가는 경로는 항상 양수이므로 별다른 문제가 되지 않습니다.
구현을 위해 우선 필요한 것은 출발점으로부터 다른 모든 정점까지의 최단거리 값이 담길 딕셔너리 자료형이며, 최단거리 비교를 위해 노드 별 거리는 무한대로 초기화합니다.
그리고 출발 노드의 값을 세팅하는데, 자기 자신이므로 0입니다. 이후 다음 노드들을 경유하면서 각 노드까지의 간선 가중치 합을 구해 기존의 거리와 비교하며 최단거리를 갱신해 나갑니다.
다음 방문 노드는 우선순위 큐를 활용해 정하는데, 이것은 큐이면서도 Min 혹은 Max 자동정렬되기에 현재 노드로부터 가장 가까운 다음 노드를 순차적으로 얻을 수 있습니다. 바로 이 부분을 그리디 방식이라고 볼 수 있습니다.
실제 구현은 다음을 보시죠.
[CODE]
import sys
import heapq
INF = sys.maxsize
def solution(self, graph, start_node):
# 1. 출발점으로부터 모든 노드까지의 최단거리를 표현하는 1차원 딕셔너리 distances -> {node: distance}
# - 각 노드들에 대한 초기거리는 무한대를 의미하는 INF 로 넣기
distances = {node: INF for node in graph} # ex. {'A': INF, 'B': INF, 'C': INF, ...}
# 2. 초기화
# - distances 를 start_node 로 초기화 >> 자기 자신을 방문하는 의미이므로, Set 0
# - 방문 노드 표현을 위한 우선순위 큐 heap 을 start_node 로 초기화
# - 우선순위 큐에 삽입시 정렬을 위한 인덱스를 node list 의 거리를 나타내는 0번 값으로 하기 때문에([0, 'A'] 처럼)
# - MinHeapq 방식에 따라 가까운 거리 순으로 자동 정렬 (우선순위 큐로서의 완전 이진 트리 살펴보기)
distances[start_node] = 0 # ex. {'A': 0, 'B': INF, 'C': INF, ...}
visit_queue = []
heapq.heappush(visit_queue, [distances[start_node], start_node]) # ex. [[0, 'A']]
# 3. 모든 노드를 방문하며 start_node 로부터의 최단거리 탐색
# - heap 에 더 이상 원소가 남지 않게 되면 모든 노드를 방문한 것
while visit_queue:
# 정렬된 우선순위 큐를 이용해 가장 인접한 노드 방문!(욕심쟁이)
visited_distance, visited_node = heapq.heappop(visit_queue) # ex. 0, 'A'
# 만약 방문한 노드까지의 거리가 기존 해당 노드까지의 최단거리보다 멀다면 skip
if distances[visited_node] < visited_distance: # ex. 0 < 0 은 False 이므로 다음 로직 진행
continue
# 현재 방문한 노드에서 가장 가까운 노드들을 우선순위 큐에 append
# 우선순위 큐에 정렬된 순서가 가까운 순서이기 때문에 순차적으로 방문 가능
for destination, destination_distance in graph[visited_node].items(): # ex. 'A' -> {'B': 8, 'C': 1, 'D': 2}
# 현재 방문한 노드를 경유하여 다음 방문 후보 노드들까지의 합산 거리 계산
new_distance = visited_distance + destination_distance # ex. 'B'는 0 + 8, 'C'는 0 + 1, 'D'는 0 + 2 순서
# 방문 후보 노드에 대한 기존 거리와, 경유지를 거칠 경우 합산된 거리 간 비교
# 가중치에 따라 경유하는 상황이 더 가까운 거리일 수 있음
if new_distance < distances[destination]: # ex. 8 < INF True 이므로 갱신됨
# 만약 경유지 합산 거리가 더 가깝다면 방문할 노드까지의 최단거리를 그것으로 갱신
distances[destination] = new_distance
# 갱신된 최단경로이므로 우선순위 큐에 다음 방문할 노드로 append
# 만약 합산 거리가 기존보다 더 멀다면 append 하지 않아도 됨
# 방문한다 해도 어차피 유의미한 최단거리가 될 수 없기 때문
# - ex. [8, 'B'], [1, 'C'], [2, 'D'] 순으로 삽입되나,
# - 우선순위 큐의 특성에 따라 각 0번 인덱스 값으로 정렬됨. C -> D -> B
heapq.heappush(visit_queue, [new_distance, destination])
return distances
상세하게 설명된 주석들을 참조하며 코드를 분석해 보세요. 예시 입력과 출력은 다음과 같습니다.
[In]
>>> graph = {
... 'A': {'B': 8, 'C': 1, 'D': 2},
... 'B': {},
... 'C': {'B': 5, 'D': 2},
... 'D': {'E': 3, 'F': 5},
... 'E': {'F': 1},
... 'F': {'A': 5}
... }
>>> print(solution(graph=graph, start_node='A'))
가중 방향 그래프를 정의하고, 출발 노드는 ‘A’로 입력하였습니다.
[Out]
{'A': 0, 'B': 6, 'C': 1, 'D': 2, 'E': 5, 'F': 6}
‘A’ 노드로부터 다른 모든 노드까지의 최단거리 자료구조가 출력됩니다.
시간복잡도의 Big-oh 방식은 알고리즘 성능에 있어서 최악의 경우를 고려하는 것이므로
주어진 E개의 모든 노드를 방문하게 될 때의 시간 복잡도 O(E)
입니다.
또한 다음 방문할 노드를 결정하는 데 priority queue 를 사용하게 되는데,
이것의 시간 복잡도는 O(logN)
입니다.
따라서 인접 노드 방문 시 매번 간선 가중치의 합으로 최단거리가 갱신되면서
다음 노드가 추가될 때의 시간 복잡도 O(ElogE)
가 있게 됩니다.
결국 우선순의 큐를 이용한 다익스트라 알고리즘의 시간 복잡도는 O(ElogE)
입니다.